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
64 changes: 62 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,33 @@ drover run --epic epic-a1b2

- Go 1.22+
- Git
- [Claude Code CLI](https://claude.ai/code) installed and authenticated
- AI Agent CLI (Claude Code or OpenCode) installed and authenticated
- PostgreSQL (production) or SQLite (local dev, default)

#### Supported AI Agents

Drover supports two AI agents for task execution:

**Claude Code (Default)**
```bash
# Install
curl -sSL https://claude.com/install | sh

# Authenticate
claude auth login
```

**OpenCode (Alternative)**
```bash
# Install
curl -fsSL https://opencode.ai/install | bash

# Authenticate (supports Anthropic, OpenAI, Google, etc.)
opencode auth login
```

Both agents work identically with Drover. Use whichever you prefer.

### From Source

```bash
Expand All @@ -83,6 +107,8 @@ go install github.com/cloud-shuttle/drover@latest
| `drover run` | Execute all tasks to completion |
| `drover run --workers 8` | Run with 8 parallel agents |
| `drover run --epic <id>` | Run only tasks in specific epic |
| `drover run --agent-type opencode` | Run with OpenCode instead of Claude Code |
| `drover run --opencode-model anthropic/claude-sonnet-4-20250514` | Specify OpenCode model |
| `drover add <title>` | Add a new task |
| `drover add <title> --parent <id>` | Add a sub-task to parent |
| `drover add "task-123.N title"` | Add sub-task with hierarchical syntax |
Expand All @@ -104,8 +130,42 @@ export DROVER_DATABASE_URL="postgresql://localhost/drover"

# Or use SQLite explicitly
export DROVER_DATABASE_URL="sqlite:///.drover.db"

# Agent selection (default: claude-code)
export DROVER_AGENT_TYPE="opencode" # "claude-code" or "opencode"
export DROVER_OPENCODE_MODEL="anthropic/claude-sonnet-4-20250514"
export DROVER_OPENCODE_PATH="/usr/local/bin/opencode"
export DROVER_OPENCODE_URL="http://localhost:4096" # Remote server for parallel execution
```

### Agent Selection

Drover defaults to Claude Code but supports OpenCode as an alternative. You can switch agents via flags or environment variables:

```bash
# Use Claude Code (default)
drover run

# Use OpenCode with specific model
drover run --agent-type opencode --opencode-model anthropic/claude-sonnet-4-20250514

# Use OpenCode with remote server (for better parallel execution)
drover run --agent-type opencode --opencode-url http://localhost:4096
```

#### OpenCode Model Format

OpenCode uses `provider/model` format for model selection. Some common providers:

| Provider | Example Model |
|----------|---------------|
| Anthropic | `anthropic/claude-sonnet-4-20250514` |
| OpenAI | `openai/gpt-4o` |
| Google | `google/gemini-2.5-pro` |
| OpenCode (free) | `opencode/grok-code` |

Use `opencode models --refresh` to list all available models from your configured providers.

### Observability

Drover includes built-in OpenTelemetry observability for production monitoring:
Expand Down Expand Up @@ -349,7 +409,7 @@ Drover is built on a pure Go stack:
| CLI | Cobra | Command-line interface |
| Workflows | DBOS Go | Durable execution |
| Database | PostgreSQL/SQLite | State persistence |
| AI Agent | Claude Code | Task execution |
| AI Agent | Claude Code / OpenCode | Task execution |
| Isolation | Git Worktrees | Parallel workspaces |
| Observability | OpenTelemetry | Traces & metrics |

Expand Down
53 changes: 41 additions & 12 deletions cmd/drover/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (
"github.com/cloud-shuttle/drover/internal/db"
"github.com/cloud-shuttle/drover/internal/git"
"github.com/cloud-shuttle/drover/internal/template"
"github.com/cloud-shuttle/drover/pkg/types"
"github.com/cloud-shuttle/drover/internal/workflow"
"github.com/cloud-shuttle/drover/pkg/types"
"github.com/dbos-inc/dbos-transact-golang/dbos"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -122,15 +122,22 @@ func runCmd() *cobra.Command {
var workers int
var epicID string
var verbose bool
var agentType string
var opencodeModel string
var opencodeURL string

cmd := &cobra.Command{
Use: "run",
Short: "Execute all tasks to completion",
Long: `Run all tasks to completion using parallel Claude Code agents.
Long: `Run all tasks to completion using parallel AI agents.

Tasks are executed respecting dependencies and priorities. Use --workers
to control parallelism. Use --epic to filter execution to a specific epic.

Agent Types:
- claude-code (default): Use Claude Code CLI
- opencode: Use OpenCode CLI

DBOS Workflow Engine:
- Default: SQLite-based orchestration (zero setup)
- With DBOS_SYSTEM_DATABASE_URL: DBOS with PostgreSQL (production mode)`,
Expand All @@ -141,13 +148,31 @@ DBOS Workflow Engine:
}
defer store.Close()

// Override config if workers flag specified
// Override config if flags specified
runCfg := *cfg
if workers > 0 {
runCfg.Workers = workers
}
runCfg.Verbose = verbose

// Set agent type
if agentType != "" {
runCfg.AgentType = config.AgentType(agentType)
}

// Validate and set OpenCode model if using OpenCode
if runCfg.AgentType == config.AgentTypeOpenCode {
if opencodeModel != "" {
if err := config.ValidateOpenCodeModel(opencodeModel); err != nil {
return fmt.Errorf("invalid OpenCode model: %w", err)
}
runCfg.OpenCodeModel = opencodeModel
}
if opencodeURL != "" {
runCfg.OpenCodeURL = opencodeURL
}
}

// Check if DBOS mode is enabled via environment variable
dbosURL := os.Getenv("DBOS_SYSTEM_DATABASE_URL")

Expand All @@ -164,6 +189,9 @@ DBOS Workflow Engine:
cmd.Flags().IntVarP(&workers, "workers", "w", 0, "Number of parallel workers")
cmd.Flags().StringVar(&epicID, "epic", "", "Filter to specific epic")
cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging for debugging")
cmd.Flags().StringVar(&agentType, "agent-type", "", "Agent type to use: claude-code or opencode")
cmd.Flags().StringVar(&opencodeModel, "opencode-model", "", "OpenCode model in format provider/model (e.g., anthropic/claude-sonnet-4-20250514)")
cmd.Flags().StringVar(&opencodeURL, "opencode-url", "", "OpenCode server URL for remote execution")

return cmd
}
Expand Down Expand Up @@ -279,11 +307,11 @@ func runWithSQLite(cmd *cobra.Command, runCfg *config.Config, store *db.Store, p

func addCmd() *cobra.Command {
var (
desc string
epicID string
parentID string
priority int
blockedBy []string
desc string
epicID string
parentID string
priority int
blockedBy []string
skipValidation bool
)

Expand All @@ -301,7 +329,7 @@ Hierarchical Tasks:
drover add "Sub-task title" --parent task-123

Maximum depth is 2 levels (Epic → Parent → Child).`,
Args: cobra.ExactArgs(1),
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
_, store, err := requireProject()
if err != nil {
Expand Down Expand Up @@ -541,10 +569,10 @@ and other metadata. Useful for inspecting individual task details.`,

func resetCmd() *cobra.Command {
var (
resetCompleted bool
resetCompleted bool
resetInProgress bool
resetClaimed bool
resetFailed bool
resetClaimed bool
resetFailed bool
)

command := &cobra.Command{
Expand Down Expand Up @@ -984,6 +1012,7 @@ func formatTimestamp(timestamp int64) string {
t := time.Unix(timestamp, 0)
return t.Format("2006-01-02 15:04:05")
}

// worktreeCmd returns the worktree management command
func worktreeCmd() *cobra.Command {
cmd := &cobra.Command{
Expand Down
87 changes: 70 additions & 17 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,18 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
)

// AgentType represents the AI agent to use for task execution
type AgentType string

const (
AgentTypeClaudeCode AgentType = "claude-code"
AgentTypeOpenCode AgentType = "opencode"
)

// Config holds Drover configuration
type Config struct {
// Database connection
Expand All @@ -21,16 +30,21 @@ type Config struct {
MaxTaskAttempts int

// Retry settings
ClaimTimeout time.Duration
StallTimeout time.Duration
PollInterval time.Duration
AutoUnblock bool
ClaimTimeout time.Duration
StallTimeout time.Duration
PollInterval time.Duration
AutoUnblock bool

// Git settings
WorktreeDir string

// Claude settings
ClaudePath string
// Agent settings
AgentType AgentType // "claude-code" or "opencode"
ClaudePath string // Path to Claude CLI (default: "claude")
OpenCodePath string // Path to OpenCode CLI (default: "opencode")
OpenCodeModel string // Model in format "provider/model" (e.g., "anthropic/claude-sonnet-4-20250514")
OpenCodeURL string // Optional remote OpenCode server URL
MergeTargetBranch string // Branch to merge changes to (default: "main")

// Beads sync settings
AutoSyncBeads bool
Expand All @@ -45,17 +59,21 @@ type Config struct {
// Load loads configuration from environment and defaults
func Load() (*Config, error) {
cfg := &Config{
DatabaseURL: defaultDatabaseURL(),
Workers: 3,
TaskTimeout: 60 * time.Minute,
MaxTaskAttempts: 3,
ClaimTimeout: 5 * time.Minute,
StallTimeout: 5 * time.Minute,
PollInterval: 2 * time.Second,
AutoUnblock: true,
WorktreeDir: ".drover/worktrees",
ClaudePath: "claude",
AutoSyncBeads: false, // Default to off for backwards compatibility
DatabaseURL: defaultDatabaseURL(),
Workers: 3,
TaskTimeout: 60 * time.Minute,
MaxTaskAttempts: 3,
ClaimTimeout: 5 * time.Minute,
StallTimeout: 5 * time.Minute,
PollInterval: 2 * time.Second,
AutoUnblock: true,
WorktreeDir: ".drover/worktrees",
ClaudePath: "claude",
OpenCodePath: "opencode",
OpenCodeModel: "anthropic/claude-sonnet-4-20250514",
AgentType: AgentTypeClaudeCode,
MergeTargetBranch: "main",
AutoSyncBeads: false,
}

// Environment overrides
Expand All @@ -71,6 +89,24 @@ func Load() (*Config, error) {
if v := os.Getenv("DROVER_AUTO_SYNC_BEADS"); v != "" {
cfg.AutoSyncBeads = v == "true" || v == "1"
}
if v := os.Getenv("DROVER_AGENT_TYPE"); v != "" {
cfg.AgentType = AgentType(v)
}
if v := os.Getenv("DROVER_CLAUDE_PATH"); v != "" {
cfg.ClaudePath = v
}
if v := os.Getenv("DROVER_OPENCODE_PATH"); v != "" {
cfg.OpenCodePath = v
}
if v := os.Getenv("DROVER_OPENCODE_MODEL"); v != "" {
cfg.OpenCodeModel = v
}
if v := os.Getenv("DROVER_OPENCODE_URL"); v != "" {
cfg.OpenCodeURL = v
}
if v := os.Getenv("DROVER_MERGE_TARGET_BRANCH"); v != "" {
cfg.MergeTargetBranch = v
}

return cfg, nil
}
Expand Down Expand Up @@ -99,3 +135,20 @@ func parseDurationOrDefault(s string, def time.Duration) time.Duration {
}
return d
}

// ValidateOpenCodeModel validates that the model format is "provider/model"
func ValidateOpenCodeModel(model string) error {
parts := strings.Split(model, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("invalid OpenCode model format: %s (expected provider/model, e.g., anthropic/claude-sonnet-4-20250514)", model)
}
return nil
}

// GetAgentExecutorPath returns the path to the agent CLI based on agent type
func (c *Config) GetAgentExecutorPath() string {
if c.AgentType == AgentTypeOpenCode {
return c.OpenCodePath
}
return c.ClaudePath
}
Loading