From 894a54b6952587991f5ac46d25fdb9cacb885f40 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Mon, 2 Mar 2026 10:39:13 -0500 Subject: [PATCH 1/5] feat(agent): add pi agent support --- cmd/roborev/main.go | 2 +- cmd/roborev/review.go | 2 +- internal/agent/agent.go | 6 +- internal/agent/pi.go | 168 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 internal/agent/pi.go diff --git a/cmd/roborev/main.go b/cmd/roborev/main.go index 27469b14..0415dc2b 100644 --- a/cmd/roborev/main.go +++ b/cmd/roborev/main.go @@ -15,7 +15,7 @@ func main() { rootCmd := &cobra.Command{ Use: "roborev", Short: "Automatic code review for git commits", - Long: "roborev automatically reviews git commits using AI agents (Codex, Claude Code, Gemini, Copilot, OpenCode, Cursor)", + Long: "roborev automatically reviews git commits using AI agents (Codex, Claude Code, Gemini, Copilot, OpenCode, Cursor, Pi)", } rootCmd.PersistentFlags().StringVar(&serverAddr, "server", "http://127.0.0.1:7373", "daemon server address") diff --git a/cmd/roborev/review.go b/cmd/roborev/review.go index 43b5f657..0d920a44 100644 --- a/cmd/roborev/review.go +++ b/cmd/roborev/review.go @@ -322,7 +322,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, cursor, kilo)") + cmd.Flags().StringVar(&agent, "agent", "", "agent to use (codex, claude-code, gemini, copilot, opencode, cursor, kilo, droid, 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().BoolVar(&fast, "fast", false, "shorthand for --reasoning fast") diff --git a/internal/agent/agent.go b/internal/agent/agent.go index 662787b6..1cb63e7d 100644 --- a/internal/agent/agent.go +++ b/internal/agent/agent.go @@ -177,8 +177,8 @@ func GetAvailable(preferred string) (Agent, error) { return Get(preferred) } - // Fallback order: codex, claude-code, gemini, copilot, opencode, cursor, kilo, droid - fallbacks := []string{"codex", "claude-code", "gemini", "copilot", "opencode", "cursor", "kilo", "droid"} + // Fallback order: codex, claude-code, gemini, copilot, opencode, cursor, kilo, droid, pi + fallbacks := []string{"codex", "claude-code", "gemini", "copilot", "opencode", "cursor", "kilo", "droid", "pi"} for _, name := range fallbacks { if name != preferred && IsAvailable(name) { return Get(name) @@ -194,7 +194,7 @@ func GetAvailable(preferred string) (Agent, error) { } if len(available) == 0 { - return nil, fmt.Errorf("no agents available (install one of: codex, claude-code, gemini, copilot, opencode, cursor, kilo, droid)\nYou may need to run 'roborev daemon restart' from a shell that has access to your agents") + return nil, fmt.Errorf("no agents available (install one of: codex, claude-code, gemini, copilot, opencode, cursor, kilo, droid, pi)\nYou may need to run 'roborev daemon restart' from a shell that has access to your agents") } return Get(available[0]) diff --git a/internal/agent/pi.go b/internal/agent/pi.go new file mode 100644 index 00000000..29535269 --- /dev/null +++ b/internal/agent/pi.go @@ -0,0 +1,168 @@ +package agent + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/exec" + "strings" +) + +// PiAgent runs code reviews using the pi CLI +type PiAgent struct { + Command string // The pi command to run (default: "pi") + Model string // Model to use (provider/model format or just model) + Reasoning ReasoningLevel // Reasoning level + Agentic bool // Agentic mode +} + +// NewPiAgent creates a new pi agent +func NewPiAgent(command string) *PiAgent { + if command == "" { + command = "pi" + } + return &PiAgent{Command: command, Reasoning: ReasoningStandard} +} + +func (a *PiAgent) Name() string { + return "pi" +} + +// WithReasoning returns a copy of the agent configured with the specified reasoning level. +func (a *PiAgent) WithReasoning(level ReasoningLevel) Agent { + return &PiAgent{ + Command: a.Command, + Model: a.Model, + Reasoning: level, + Agentic: a.Agentic, + } +} + +// WithAgentic returns a copy of the agent configured for agentic mode. +func (a *PiAgent) WithAgentic(agentic bool) Agent { + return &PiAgent{ + Command: a.Command, + Model: a.Model, + Reasoning: a.Reasoning, + Agentic: agentic, + } +} + +// WithModel returns a copy of the agent configured to use the specified model. +func (a *PiAgent) WithModel(model string) Agent { + if model == "" { + return a + } + return &PiAgent{ + Command: a.Command, + Model: model, + Reasoning: a.Reasoning, + Agentic: a.Agentic, + } +} + +func (a *PiAgent) CommandName() string { + return a.Command +} + +func (a *PiAgent) CommandLine() string { + args := []string{"-p"} + if a.Model != "" { + args = append(args, "--model", a.Model) + } + if level := a.thinkingLevel(); level != "" { + args = append(args, "--thinking", level) + } + return a.Command + " " + strings.Join(args, " ") +} + +func (a *PiAgent) thinkingLevel() string { + switch a.Reasoning { + case ReasoningThorough: + return "high" + case ReasoningFast: + return "low" + default: // Standard + return "medium" + } +} + +func (a *PiAgent) Review( + ctx context.Context, + repoPath, commitSHA, prompt string, + output io.Writer, +) (string, error) { + // Write prompt to a temporary file to avoid command line length limits + // and to properly handle special characters. + tmpDir := os.TempDir() + tmpFile, err := os.CreateTemp(tmpDir, "roborev-pi-prompt-*.md") + if err != nil { + return "", fmt.Errorf("create temp prompt file: %w", err) + } + defer os.Remove(tmpFile.Name()) + + if _, err := tmpFile.WriteString(prompt); err != nil { + tmpFile.Close() + return "", fmt.Errorf("write prompt to temp file: %w", err) + } + tmpFile.Close() + + args := []string{"-p"} // Print response and exit + if a.Model != "" { + args = append(args, "--model", a.Model) + } + if level := a.thinkingLevel(); level != "" { + args = append(args, "--thinking", level) + } + + // Add the prompt file as an input argument (prefixed with @) + // Pi treats @files as context/input. + // Since the prompt contains the instructions, we pass it as a file. + // But pi might expect instructions as text arguments. + // The docs say: pi [options] [@files...] [messages...] + // If we only provide @file, pi reads it. Does it treat it as a user message or context? + // Usually @file is context. + // We want the prompt to be the "message". + // But if the prompt is huge, we can't pass it as an argument. + // If we pass @file, pi loads the file. + // We might need to add a small trigger message like "Follow the instructions in the attached file." + args = append(args, "@"+tmpFile.Name(), "Please follow the instructions in the attached file to review the code.") + + cmd := exec.CommandContext(ctx, a.Command, args...) + cmd.Dir = repoPath + + // Capture stdout for the result + var stdoutBuf bytes.Buffer + var stderrBuf bytes.Buffer + + // Stream stdout to output writer if provided + if output != nil { + sw := newSyncWriter(output) + cmd.Stdout = io.MultiWriter(&stdoutBuf, sw) + cmd.Stderr = io.MultiWriter(&stderrBuf, sw) + } else { + cmd.Stdout = &stdoutBuf + cmd.Stderr = &stderrBuf + } + + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("pi failed: %w\nstderr: %s", err, stderrBuf.String()) + } + + result := stdoutBuf.String() + if result == "" { + // Fallback to stderr if stdout is empty (though -p should print to stdout) + if stderrBuf.Len() > 0 { + return stderrBuf.String(), nil + } + return "No review output generated", nil + } + + return result, nil +} + +func init() { + Register(NewPiAgent("")) +} From 49b98380bd44c435981243ddb98ab2d1257ed779 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Mon, 2 Mar 2026 10:40:36 -0500 Subject: [PATCH 2/5] feat(agent): add provider support for pi agent --- cmd/roborev/review.go | 13 +++++++++++-- internal/agent/pi.go | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/cmd/roborev/review.go b/cmd/roborev/review.go index 0d920a44..d473e505 100644 --- a/cmd/roborev/review.go +++ b/cmd/roborev/review.go @@ -36,6 +36,7 @@ func reviewCmd() *cobra.Command { baseBranch string since string local bool + provider string ) cmd := &cobra.Command{ @@ -250,7 +251,7 @@ Examples: // Handle --local mode: run agent directly without daemon if local { - return runLocalReview(cmd, root, gitRef, diffContent, agent, model, reasoning, reviewType, quiet) + return runLocalReview(cmd, root, gitRef, diffContent, agent, model, provider, reasoning, reviewType, quiet) } // Build request body @@ -335,6 +336,7 @@ Examples: cmd.Flags().StringVar(&since, "since", "", "review commits since this commit (exclusive, like git's .. range)") cmd.Flags().BoolVar(&local, "local", false, "run review locally without daemon (streams output to console)") cmd.Flags().StringVar(&reviewType, "type", "", "review type (security, design) — changes system prompt") + cmd.Flags().StringVar(&provider, "provider", "", "provider for pi agent (e.g. anthropic, openai)") registerAgentCompletion(cmd) registerReasoningCompletion(cmd) @@ -342,7 +344,7 @@ Examples: } // runLocalReview runs a review directly without the daemon -func runLocalReview(cmd *cobra.Command, repoPath, gitRef, diffContent, agentName, model, reasoning, reviewType string, quiet bool) error { +func runLocalReview(cmd *cobra.Command, repoPath, gitRef, diffContent, agentName, model, provider, reasoning, reviewType string, quiet bool) error { // Load config cfg, err := config.LoadGlobal() if err != nil { @@ -377,6 +379,13 @@ func runLocalReview(cmd *cobra.Command, repoPath, gitRef, diffContent, agentName reasoningLevel := agent.ParseReasoningLevel(reasoning) a = a.WithReasoning(reasoningLevel).WithModel(model) + // Configure provider for pi agent + if provider != "" { + if pa, ok := a.(*agent.PiAgent); ok { + a = pa.WithProvider(provider) + } + } + // Use consistent output writer, respecting --quiet var out = cmd.OutOrStdout() if quiet { diff --git a/internal/agent/pi.go b/internal/agent/pi.go index 29535269..2889d409 100644 --- a/internal/agent/pi.go +++ b/internal/agent/pi.go @@ -14,6 +14,7 @@ import ( type PiAgent struct { Command string // The pi command to run (default: "pi") Model string // Model to use (provider/model format or just model) + Provider string // Explicit provider (optional) Reasoning ReasoningLevel // Reasoning level Agentic bool // Agentic mode } @@ -35,6 +36,7 @@ func (a *PiAgent) WithReasoning(level ReasoningLevel) Agent { return &PiAgent{ Command: a.Command, Model: a.Model, + Provider: a.Provider, Reasoning: level, Agentic: a.Agentic, } @@ -45,6 +47,7 @@ func (a *PiAgent) WithAgentic(agentic bool) Agent { return &PiAgent{ Command: a.Command, Model: a.Model, + Provider: a.Provider, Reasoning: a.Reasoning, Agentic: agentic, } @@ -58,6 +61,21 @@ func (a *PiAgent) WithModel(model string) Agent { return &PiAgent{ Command: a.Command, Model: model, + Provider: a.Provider, + Reasoning: a.Reasoning, + Agentic: a.Agentic, + } +} + +// WithProvider returns a copy of the agent configured to use the specified provider. +func (a *PiAgent) WithProvider(provider string) Agent { + if provider == "" { + return a + } + return &PiAgent{ + Command: a.Command, + Model: a.Model, + Provider: provider, Reasoning: a.Reasoning, Agentic: a.Agentic, } @@ -69,6 +87,9 @@ func (a *PiAgent) CommandName() string { func (a *PiAgent) CommandLine() string { args := []string{"-p"} + if a.Provider != "" { + args = append(args, "--provider", a.Provider) + } if a.Model != "" { args = append(args, "--model", a.Model) } @@ -110,6 +131,9 @@ func (a *PiAgent) Review( tmpFile.Close() args := []string{"-p"} // Print response and exit + if a.Provider != "" { + args = append(args, "--provider", a.Provider) + } if a.Model != "" { args = append(args, "--model", a.Model) } From fc68ad7c899bb9e5530d18923e8637334cea0407 Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Mon, 2 Mar 2026 10:41:41 -0500 Subject: [PATCH 3/5] feat(config): add pi_cmd to config --- internal/config/config.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index 68ff797d..395fff3e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -107,6 +107,7 @@ type Config struct { CodexCmd string `toml:"codex_cmd"` ClaudeCodeCmd string `toml:"claude_code_cmd"` CursorCmd string `toml:"cursor_cmd"` + PiCmd string `toml:"pi_cmd"` // API keys (optional - agents use subscription auth by default) AnthropicAPIKey string `toml:"anthropic_api_key" sensitive:"true"` @@ -477,6 +478,7 @@ func DefaultConfig() *Config { CodexCmd: "codex", ClaudeCodeCmd: "claude", CursorCmd: "agent", + PiCmd: "pi", } } From 4bcb1b9f0e1e2501ab0da85e27b4ad881c53a79d Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Mon, 2 Mar 2026 10:42:52 -0500 Subject: [PATCH 4/5] feat(agent): update GetAvailableWithConfig to support pi command override --- internal/agent/acp.go | 45 +++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/internal/agent/acp.go b/internal/agent/acp.go index ace326d3..996e5e15 100644 --- a/internal/agent/acp.go +++ b/internal/agent/acp.go @@ -1508,39 +1508,46 @@ func configuredACPAgent(cfg *config.Config) *ACPAgent { // GetAvailableWithConfig resolves an available agent while honoring runtime ACP config. // It treats cfg.ACP.Name as an alias for "acp" and applies cfg.ACP command/mode/model // at resolution time instead of package-init time. +// It also applies command overrides for other agents (codex, claude, cursor, pi). func GetAvailableWithConfig(preferred string, cfg *config.Config) (Agent, error) { preferred = resolveAlias(strings.TrimSpace(preferred)) - if isConfiguredACPAgentName(preferred, cfg) { + if cfg != nil && cfg.ACP != nil && isConfiguredACPAgentName(preferred, cfg) { acpAgent := configuredACPAgent(cfg) if _, err := exec.LookPath(acpAgent.CommandName()); err == nil { return acpAgent, nil } - // ACP requested with an invalid configured command. Try canonical ACP next. - if canonicalACP, err := Get(defaultACPName); err == nil { - if commandAgent, ok := canonicalACP.(CommandAgent); !ok { - return canonicalACP, nil - } else if _, err := exec.LookPath(commandAgent.CommandName()); err == nil { - return canonicalACP, nil - } - } - - // Finally fall back to normal auto-selection. - return GetAvailable("") } - resolved, err := GetAvailable(preferred) + // Resolve the agent first + a, err := GetAvailable(preferred) if err != nil { return nil, err } - if resolved.Name() == defaultACPName { - configured := configuredACPAgent(cfg) - if _, err := exec.LookPath(configured.CommandName()); err == nil { - return configured, nil + + // Apply command overrides from config + if cfg != nil { + switch agent := a.(type) { + case *CodexAgent: + if cfg.CodexCmd != "" { + agent.Command = cfg.CodexCmd + } + case *ClaudeAgent: + if cfg.ClaudeCodeCmd != "" { + agent.Command = cfg.ClaudeCodeCmd + } + case *CursorAgent: + if cfg.CursorCmd != "" { + agent.Command = cfg.CursorCmd + } + case *PiAgent: + if cfg.PiCmd != "" { + agent.Command = cfg.PiCmd + } } - return resolved, nil } - return resolved, nil + + return a, nil } func applyACPAgentConfigOverride(cfg *config.ACPAgentConfig, override *config.ACPAgentConfig) { From 5247d4c64a4e4158e5ff40bb5b2e38c9a1e1c7ea Mon Sep 17 00:00:00 2001 From: Marius van Niekerk Date: Mon, 2 Mar 2026 12:32:54 -0500 Subject: [PATCH 5/5] feat: wire provider field through daemon/storage for pi agent - Add provider field to ReviewJob, EnqueueOpts, and EnqueueRequest - Store provider in review_jobs table with migration - Pass provider from CLI --provider flag through daemon enqueue - Apply provider in worker when agent is PiAgent via WithProvider --- cmd/roborev/review.go | 1 + internal/daemon/server.go | 5 +++++ internal/daemon/worker.go | 7 +++++++ internal/storage/db.go | 15 ++++++++++++++- internal/storage/jobs.go | 17 +++++++++++------ internal/storage/models.go | 1 + 6 files changed, 39 insertions(+), 7 deletions(-) diff --git a/cmd/roborev/review.go b/cmd/roborev/review.go index d473e505..ccdac4af 100644 --- a/cmd/roborev/review.go +++ b/cmd/roborev/review.go @@ -261,6 +261,7 @@ Examples: Branch: branchName, Agent: agent, Model: model, + Provider: provider, Reasoning: reasoning, ReviewType: reviewType, DiffContent: diffContent, diff --git a/internal/daemon/server.go b/internal/daemon/server.go index bc92ae58..2a92cd3c 100644 --- a/internal/daemon/server.go +++ b/internal/daemon/server.go @@ -459,6 +459,7 @@ type EnqueueRequest struct { Agentic bool `json:"agentic,omitempty"` // Enable agentic mode (allow file edits) OutputPrefix string `json:"output_prefix,omitempty"` // Prefix to prepend to review output JobType string `json:"job_type,omitempty"` // Explicit job type (review/range/dirty/task/compact) + Provider string `json:"provider,omitempty"` // Provider for pi agent (e.g., "anthropic") } type ErrorResponse struct { @@ -651,6 +652,7 @@ func (s *Server) handleEnqueue(w http.ResponseWriter, r *http.Request) { Agentic: req.Agentic, Label: gitRef, // Use git_ref as TUI label (run, analyze type, custom) JobType: req.JobType, + Provider: req.Provider, }) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("enqueue prompt job: %v", err)) @@ -667,6 +669,7 @@ func (s *Server) handleEnqueue(w http.ResponseWriter, r *http.Request) { Reasoning: reasoning, ReviewType: req.ReviewType, DiffContent: req.DiffContent, + Provider: req.Provider, }) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("enqueue dirty job: %v", err)) @@ -709,6 +712,7 @@ func (s *Server) handleEnqueue(w http.ResponseWriter, r *http.Request) { Model: model, Reasoning: reasoning, ReviewType: req.ReviewType, + Provider: req.Provider, }) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("enqueue job: %v", err)) @@ -748,6 +752,7 @@ func (s *Server) handleEnqueue(w http.ResponseWriter, r *http.Request) { Reasoning: reasoning, ReviewType: req.ReviewType, PatchID: patchID, + Provider: req.Provider, }) if err != nil { writeError(w, http.StatusInternalServerError, fmt.Sprintf("enqueue job: %v", err)) diff --git a/internal/daemon/worker.go b/internal/daemon/worker.go index 3235c7b9..00bb9864 100644 --- a/internal/daemon/worker.go +++ b/internal/daemon/worker.go @@ -402,6 +402,13 @@ func (wp *WorkerPool) processJob(workerID string, job *storage.ReviewJob) { reasoningLevel := agent.ParseReasoningLevel(reasoning) a := baseAgent.WithReasoning(reasoningLevel).WithAgentic(job.Agentic).WithModel(job.Model) + // Apply provider if set and agent supports it (e.g. pi agent) + if job.Provider != "" { + if pa, ok := a.(*agent.PiAgent); ok { + a = pa.WithProvider(job.Provider) + } + } + // Use the actual agent name (may differ from requested if fallback occurred) agentName := a.Name() if agentName != job.Agent { diff --git a/internal/storage/db.go b/internal/storage/db.go index b3e1a0ba..1a93a7e5 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -52,7 +52,8 @@ CREATE TABLE IF NOT EXISTS review_jobs ( diff_content TEXT, output_prefix TEXT, job_type TEXT NOT NULL DEFAULT 'review', - review_type TEXT NOT NULL DEFAULT '' + review_type TEXT NOT NULL DEFAULT '', + provider TEXT ); CREATE TABLE IF NOT EXISTS reviews ( @@ -655,6 +656,18 @@ func (db *DB) migrate() error { } } + // Migration: add provider column to review_jobs if missing + err = db.QueryRow(`SELECT COUNT(*) FROM pragma_table_info('review_jobs') WHERE name = 'provider'`).Scan(&count) + if err != nil { + return fmt.Errorf("check provider column: %w", err) + } + if count == 0 { + _, err = db.Exec(`ALTER TABLE review_jobs ADD COLUMN provider TEXT`) + if err != nil { + return fmt.Errorf("add provider column: %w", err) + } + } + // Run sync-related migrations if err := db.migrateSyncColumns(); err != nil { return err diff --git a/internal/storage/jobs.go b/internal/storage/jobs.go index 81a797cb..4b52a440 100644 --- a/internal/storage/jobs.go +++ b/internal/storage/jobs.go @@ -739,6 +739,7 @@ type EnqueueOpts struct { Branch string Agent string Model string + Provider string // e.g. "anthropic", "openai" Reasoning string ReviewType string // e.g. "security" — changes which system prompt is used PatchID string // Stable patch-id for rebase tracking @@ -808,12 +809,12 @@ func (db *DB) EnqueueJob(opts EnqueueOpts) (*ReviewJob, error) { } result, err := db.Exec(` - INSERT INTO review_jobs (repo_id, commit_id, git_ref, branch, agent, model, reasoning, + INSERT INTO review_jobs (repo_id, commit_id, git_ref, branch, agent, model, provider, reasoning, status, job_type, review_type, patch_id, diff_content, prompt, agentic, output_prefix, parent_job_id, uuid, source_machine_id, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, opts.RepoID, commitIDParam, gitRef, nullString(opts.Branch), - opts.Agent, nullString(opts.Model), reasoning, + opts.Agent, nullString(opts.Model), nullString(opts.Provider), reasoning, jobType, opts.ReviewType, nullString(opts.PatchID), nullString(opts.DiffContent), nullString(opts.Prompt), agenticInt, nullString(opts.OutputPrefix), parentJobIDParam, @@ -830,6 +831,7 @@ func (db *DB) EnqueueJob(opts EnqueueOpts) (*ReviewJob, error) { Branch: opts.Branch, Agent: opts.Agent, Model: opts.Model, + Provider: opts.Provider, Reasoning: reasoning, JobType: jobType, ReviewType: opts.ReviewType, @@ -892,7 +894,7 @@ func (db *DB) ClaimJob(workerID string) (*ReviewJob, error) { var commitSubject sql.NullString var diffContent sql.NullString var prompt sql.NullString - var model, branch sql.NullString + var model, provider, branch sql.NullString var agenticInt int var jobType sql.NullString var reviewType sql.NullString @@ -900,7 +902,7 @@ func (db *DB) ClaimJob(workerID string) (*ReviewJob, error) { var patchID sql.NullString var parentJobID sql.NullInt64 err = db.QueryRow(` - SELECT j.id, j.repo_id, j.commit_id, j.git_ref, j.branch, j.agent, j.model, j.reasoning, j.status, j.enqueued_at, + SELECT j.id, j.repo_id, j.commit_id, j.git_ref, j.branch, j.agent, j.model, j.provider, j.reasoning, j.status, j.enqueued_at, r.root_path, r.name, c.subject, j.diff_content, j.prompt, COALESCE(j.agentic, 0), j.job_type, j.review_type, j.output_prefix, j.patch_id, j.parent_job_id FROM review_jobs j @@ -909,7 +911,7 @@ func (db *DB) ClaimJob(workerID string) (*ReviewJob, error) { WHERE j.worker_id = ? AND j.status = 'running' ORDER BY j.started_at DESC LIMIT 1 - `, workerID).Scan(&job.ID, &job.RepoID, &commitID, &job.GitRef, &branch, &job.Agent, &model, &job.Reasoning, &job.Status, &enqueuedAt, + `, workerID).Scan(&job.ID, &job.RepoID, &commitID, &job.GitRef, &branch, &job.Agent, &model, &provider, &job.Reasoning, &job.Status, &enqueuedAt, &job.RepoPath, &job.RepoName, &commitSubject, &diffContent, &prompt, &agenticInt, &jobType, &reviewType, &outputPrefix, &patchID, &parentJobID) if err != nil { @@ -931,6 +933,9 @@ func (db *DB) ClaimJob(workerID string) (*ReviewJob, error) { if model.Valid { job.Model = model.String } + if provider.Valid { + job.Provider = provider.String + } if branch.Valid { job.Branch = branch.String } diff --git a/internal/storage/models.go b/internal/storage/models.go index 2064f15f..7c1d9d50 100644 --- a/internal/storage/models.go +++ b/internal/storage/models.go @@ -53,6 +53,7 @@ type ReviewJob struct { Branch string `json:"branch,omitempty"` // Branch name at time of job creation Agent string `json:"agent"` Model string `json:"model,omitempty"` // Model to use (for opencode: provider/model format) + Provider string `json:"provider,omitempty"` // Provider to use (e.g., anthropic, openai) Reasoning string `json:"reasoning,omitempty"` // thorough, standard, fast (default: thorough) JobType string `json:"job_type"` // review, range, dirty, task Status JobStatus `json:"status"`