Skip to content

Commit 451c475

Browse files
author
Assistant
committed
initial draft of opencode support using opencode free models
1 parent 4dcb3ed commit 451c475

11 files changed

Lines changed: 455 additions & 23 deletions

File tree

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,17 +44,17 @@ See the [documentation](https://minicodemonkey.github.io/chief/concepts/how-it-w
4444

4545
## Requirements
4646

47-
- **[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)** or **[Codex CLI](https://developers.openai.com/codex/cli/reference)** installed and authenticated
47+
- **[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)**, **[Codex CLI](https://developers.openai.com/codex/cli/reference)**, or **[OpenCode CLI](https://opencode.ai)** installed and authenticated
4848

49-
Use Claude by default, or configure Codex in `.chief/config.yaml`:
49+
Use Claude by default, or configure Codex or OpenCode in `.chief/config.yaml`:
5050

5151
```yaml
5252
agent:
53-
provider: codex
54-
cliPath: /usr/local/bin/codex # optional
53+
provider: opencode
54+
cliPath: /usr/local/bin/opencode # optional
5555
```
5656
57-
Or run with `chief --agent codex` or set `CHIEF_AGENT=codex`.
57+
Or run with `chief --agent opencode` or set `CHIEF_AGENT=opencode`.
5858

5959
## License
6060

docs/guide/installation.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Chief is distributed as a single binary with no runtime dependencies. Choose you
88

99
## Prerequisites
1010

11-
Chief needs an agent CLI: **Claude Code** (default) or **Codex**. Install at least one and authenticate.
11+
Chief needs an agent CLI: **Claude Code** (default), **Codex**, or **OpenCode**. Install at least one and authenticate.
1212

1313
### Option A: Claude Code CLI (default)
1414

@@ -45,6 +45,18 @@ To use [OpenAI Codex CLI](https://developers.openai.com/codex/cli/reference) ins
4545
Run `codex --version` (or your custom path) to confirm Codex is available.
4646
:::
4747

48+
### Option C: OpenCode CLI
49+
50+
To use [OpenCode CLI](https://opencode.ai) as an alternative:
51+
52+
1. Install OpenCode per the [official docs](https://opencode.ai/docs/).
53+
2. Ensure `opencode` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml` (see [Configuration](/reference/configuration#agent)).
54+
3. Run Chief with `chief --agent opencode` or set `CHIEF_AGENT=opencode`, or set `agent.provider: opencode` in `.chief/config.yaml`.
55+
56+
::: tip Verify OpenCode
57+
Run `opencode --version` (or your custom path) to confirm OpenCode is available.
58+
:::
59+
4860
### Optional: GitHub CLI (`gh`)
4961

5062
If you want Chief to automatically create pull requests when a PRD completes, install the [GitHub CLI](https://cli.github.com/):

docs/reference/configuration.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Chief stores project-level settings in `.chief/config.yaml`. This file is create
1414

1515
```yaml
1616
agent:
17-
provider: claude # or "codex"
17+
provider: claude # or "codex" or "opencode"
1818
cliPath: "" # optional path to CLI binary
1919
worktree:
2020
setup: "npm install"
@@ -27,8 +27,8 @@ onComplete:
2727
2828
| Key | Type | Default | Description |
2929
|-----|------|---------|-------------|
30-
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude` or `codex` |
31-
| `agent.cliPath` | string | `""` | Optional path to the agent binary (e.g. `/usr/local/bin/codex`). If empty, Chief uses the provider name from PATH. |
30+
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude`, `codex`, or `opencode` |
31+
| `agent.cliPath` | string | `""` | Optional path to the agent binary (e.g. `/usr/local/bin/opencode`). If empty, Chief uses the provider name from PATH. |
3232
| `worktree.setup` | string | `""` | Shell command to run in new worktrees (e.g., `npm install`, `go mod download`) |
3333
| `onComplete.push` | bool | `false` | Automatically push the branch to remote when a PRD completes |
3434
| `onComplete.createPR` | bool | `false` | Automatically create a pull request when a PRD completes (requires `gh` CLI) |
@@ -88,7 +88,7 @@ These settings are saved to `.chief/config.yaml` and can be changed at any time
8888

8989
| Flag | Description | Default |
9090
|------|-------------|---------|
91-
| `--agent <provider>` | Agent CLI to use: `claude` or `codex` | From config / env / `claude` |
91+
| `--agent <provider>` | Agent CLI to use: `claude`, `codex`, or `opencode` | From config / env / `claude` |
9292
| `--agent-path <path>` | Custom path to the agent CLI binary | From config / env |
9393
| `--max-iterations <n>`, `-n` | Loop iteration limit | Dynamic |
9494
| `--no-retry` | Disable auto-retry on agent crashes | `false` |
@@ -102,11 +102,11 @@ When `--max-iterations` is not specified, Chief calculates a dynamic limit based
102102

103103
## Agent
104104

105-
Chief can use **Claude Code** (default) or **Codex CLI** as the agent. Choose via:
105+
Chief can use **Claude Code** (default), **Codex CLI**, or **OpenCode CLI** as the agent. Choose via:
106106

107-
- **Config:** `agent.provider: codex` and optionally `agent.cliPath: /path/to/codex` in `.chief/config.yaml`
108-
- **Environment:** `CHIEF_AGENT=codex`, `CHIEF_AGENT_PATH=/path/to/codex`
109-
- **CLI:** `chief --agent codex --agent-path /path/to/codex`
107+
- **Config:** `agent.provider: opencode` and optionally `agent.cliPath: /path/to/opencode` in `.chief/config.yaml`
108+
- **Environment:** `CHIEF_AGENT=opencode`, `CHIEF_AGENT_PATH=/path/to/opencode`
109+
- **CLI:** `chief --agent opencode --agent-path /path/to/opencode`
110110

111111
## Claude Code Configuration
112112

docs/troubleshooting/common-issues.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Solutions to frequently encountered problems.
88

99
## Agent CLI Not Found
1010

11-
**Symptom:** Error that the agent CLI (Claude or Codex) is not found.
11+
**Symptom:** Error that the agent CLI (Claude, Codex, or OpenCode) is not found.
1212

1313
```
1414
Error: Claude CLI not found in PATH. Install it or set agent.cliPath in .chief/config.yaml
@@ -17,6 +17,10 @@ or
1717
```
1818
Error: Codex CLI not found in PATH. Install it or set agent.cliPath in .chief/config.yaml
1919
```
20+
or
21+
```
22+
Error: OpenCode CLI not found in PATH. Install it or set agent.cliPath in .chief/config.yaml
23+
```
2024

2125
**Cause:** The chosen agent CLI isn't installed or isn't in your PATH.
2226

@@ -33,6 +37,13 @@ Error: Codex CLI not found in PATH. Install it or set agent.cliPath in .chief/co
3337
cliPath: /usr/local/bin/codex
3438
```
3539
Verify with `codex --version` (or your `cliPath`).
40+
- **OpenCode:** Install [OpenCode CLI](https://opencode.ai/docs/) and ensure `opencode` is in PATH, or set the path in config:
41+
```yaml
42+
agent:
43+
provider: opencode
44+
cliPath: /usr/local/bin/opencode
45+
```
46+
Verify with `opencode --version` (or your `cliPath`).
3647

3748
## Permission Denied
3849

@@ -52,7 +63,7 @@ Chief automatically runs Claude with permission prompts disabled for autonomous
5263

5364
**Solution:**
5465

55-
1. Check the agent log for errors (e.g. `claude.log` or `codex.log` in the PRD directory):
66+
1. Check the agent log for errors (e.g. `claude.log`, `codex.log`, or `opencode.log` in the PRD directory):
5667
```bash
5768
tail -100 .chief/prds/your-prd/claude.log
5869
```
@@ -76,7 +87,7 @@ Chief automatically runs Claude with permission prompts disabled for autonomous
7687

7788
**Solution:**
7889

79-
1. Check the agent log (e.g. `claude.log` or `codex.log`) for what the agent is doing:
90+
1. Check the agent log (e.g. `claude.log`, `codex.log`, or `opencode.log`) for what the agent is doing:
8091
```bash
8192
tail -f .chief/prds/your-prd/claude.log
8293
```
@@ -107,7 +118,7 @@ Chief automatically runs Claude with permission prompts disabled for autonomous
107118

108119
2. Or investigate why it's taking so many iterations:
109120
- Story too complex? Split it
110-
- Stuck in a loop? Check the agent log (`claude.log` or `codex.log`)
121+
- Stuck in a loop? Check the agent log (`claude.log`, `codex.log`, or `opencode.log`)
111122
- Unclear acceptance criteria? Clarify them
112123

113124
## "No PRD Found"
@@ -249,4 +260,4 @@ If none of these solutions help:
249260
3. Open a new issue with:
250261
- Chief version (`chief --version`)
251262
- Your `prd.json` (sanitized)
252-
- Relevant agent log excerpts (e.g. `claude.log` or `codex.log`)
263+
- Relevant agent log excerpts (e.g. `claude.log`, `codex.log`, or `opencode.log`)

internal/agent/opencode.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
"strings"
7+
8+
"github.com/minicodemonkey/chief/internal/loop"
9+
)
10+
11+
type OpenCodeProvider struct {
12+
cliPath string
13+
}
14+
15+
func NewOpenCodeProvider(cliPath string) *OpenCodeProvider {
16+
if cliPath == "" {
17+
cliPath = "opencode"
18+
}
19+
return &OpenCodeProvider{cliPath: cliPath}
20+
}
21+
22+
func (p *OpenCodeProvider) Name() string { return "OpenCode" }
23+
24+
func (p *OpenCodeProvider) CLIPath() string { return p.cliPath }
25+
26+
func (p *OpenCodeProvider) LoopCommand(ctx context.Context, prompt, workDir string) *exec.Cmd {
27+
cmd := exec.CommandContext(ctx, p.cliPath, "run", "--format", "json", prompt)
28+
cmd.Dir = workDir
29+
return cmd
30+
}
31+
32+
func (p *OpenCodeProvider) InteractiveCommand(workDir, prompt string) *exec.Cmd {
33+
cmd := exec.Command(p.cliPath)
34+
cmd.Dir = workDir
35+
return cmd
36+
}
37+
38+
func (p *OpenCodeProvider) ConvertCommand(workDir, prompt string) (*exec.Cmd, loop.OutputMode, string, error) {
39+
cmd := exec.Command(p.cliPath, "run", "--format", "json", "--", prompt)
40+
cmd.Dir = workDir
41+
cmd.Stdin = strings.NewReader(prompt)
42+
return cmd, loop.OutputStdout, "", nil
43+
}
44+
45+
func (p *OpenCodeProvider) FixJSONCommand(prompt string) (*exec.Cmd, loop.OutputMode, string, error) {
46+
cmd := exec.Command(p.cliPath, "run", "--format", "json", "--", prompt)
47+
cmd.Stdin = strings.NewReader(prompt)
48+
return cmd, loop.OutputStdout, "", nil
49+
}
50+
51+
func (p *OpenCodeProvider) ParseLine(line string) *loop.Event {
52+
return loop.ParseLineOpenCode(line)
53+
}
54+
55+
func (p *OpenCodeProvider) LogFileName() string { return "opencode.log" }

internal/agent/opencode_test.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package agent
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/minicodemonkey/chief/internal/loop"
8+
)
9+
10+
func TestOpenCodeProvider_Name(t *testing.T) {
11+
p := NewOpenCodeProvider("")
12+
if p.Name() != "OpenCode" {
13+
t.Errorf("Name() = %q, want OpenCode", p.Name())
14+
}
15+
}
16+
17+
func TestOpenCodeProvider_CLIPath(t *testing.T) {
18+
p := NewOpenCodeProvider("")
19+
if p.CLIPath() != "opencode" {
20+
t.Errorf("CLIPath() empty arg = %q, want opencode", p.CLIPath())
21+
}
22+
p2 := NewOpenCodeProvider("/usr/local/bin/opencode")
23+
if p2.CLIPath() != "/usr/local/bin/opencode" {
24+
t.Errorf("CLIPath() custom = %q, want /usr/local/bin/opencode", p2.CLIPath())
25+
}
26+
}
27+
28+
func TestOpenCodeProvider_LogFileName(t *testing.T) {
29+
p := NewOpenCodeProvider("")
30+
if p.LogFileName() != "opencode.log" {
31+
t.Errorf("LogFileName() = %q, want opencode.log", p.LogFileName())
32+
}
33+
}
34+
35+
func TestOpenCodeProvider_LoopCommand(t *testing.T) {
36+
ctx := context.Background()
37+
p := NewOpenCodeProvider("/bin/opencode")
38+
cmd := p.LoopCommand(ctx, "hello world", "/work/dir")
39+
40+
if cmd.Path != "/bin/opencode" {
41+
t.Errorf("LoopCommand Path = %q, want /bin/opencode", cmd.Path)
42+
}
43+
wantArgs := []string{"/bin/opencode", "run", "--format", "json", "hello world"}
44+
if len(cmd.Args) != len(wantArgs) {
45+
t.Fatalf("LoopCommand Args len = %d, want %d: %v", len(cmd.Args), len(wantArgs), cmd.Args)
46+
}
47+
for i, w := range wantArgs {
48+
if cmd.Args[i] != w {
49+
t.Errorf("LoopCommand Args[%d] = %q, want %q", i, cmd.Args[i], w)
50+
}
51+
}
52+
if cmd.Dir != "/work/dir" {
53+
t.Errorf("LoopCommand Dir = %q, want /work/dir", cmd.Dir)
54+
}
55+
}
56+
57+
func TestOpenCodeProvider_ConvertCommand(t *testing.T) {
58+
p := NewOpenCodeProvider("opencode")
59+
cmd, mode, outPath, err := p.ConvertCommand("/prd/dir", "convert prompt")
60+
if err != nil {
61+
t.Fatalf("ConvertCommand unexpected error: %v", err)
62+
}
63+
if mode != loop.OutputStdout {
64+
t.Errorf("ConvertCommand mode = %v, want OutputStdout", mode)
65+
}
66+
if outPath != "" {
67+
t.Errorf("ConvertCommand outPath = %q, want empty string", outPath)
68+
}
69+
if cmd.Path != "opencode" {
70+
t.Errorf("ConvertCommand Path = %q, want opencode", cmd.Path)
71+
}
72+
if cmd.Dir != "/prd/dir" {
73+
t.Errorf("ConvertCommand Dir = %q, want /prd/dir", cmd.Dir)
74+
}
75+
}
76+
77+
func TestOpenCodeProvider_FixJSONCommand(t *testing.T) {
78+
p := NewOpenCodeProvider("opencode")
79+
cmd, mode, outPath, err := p.FixJSONCommand("fix prompt")
80+
if err != nil {
81+
t.Fatalf("FixJSONCommand unexpected error: %v", err)
82+
}
83+
if mode != loop.OutputStdout {
84+
t.Errorf("FixJSONCommand mode = %v, want OutputStdout", mode)
85+
}
86+
if outPath != "" {
87+
t.Errorf("FixJSONCommand outPath = %q, want empty string", outPath)
88+
}
89+
}
90+
91+
func TestOpenCodeProvider_InteractiveCommand(t *testing.T) {
92+
p := NewOpenCodeProvider("opencode")
93+
cmd := p.InteractiveCommand("/work", "my prompt")
94+
if cmd.Dir != "/work" {
95+
t.Errorf("InteractiveCommand Dir = %q, want /work", cmd.Dir)
96+
}
97+
}
98+
99+
func TestOpenCodeProvider_ParseLine(t *testing.T) {
100+
p := NewOpenCodeProvider("")
101+
e := p.ParseLine(`{"type":"step_start","timestamp":1234567890,"sessionID":"ses_test123"}`)
102+
if e == nil {
103+
t.Fatal("ParseLine(step_start) returned nil")
104+
}
105+
if e.Type != loop.EventIterationStart {
106+
t.Errorf("ParseLine(step_start) Type = %v, want EventIterationStart", e.Type)
107+
}
108+
}

internal/agent/resolve.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ func Resolve(flagAgent, flagPath string, cfg *config.Config) (loop.Provider, err
3737
return NewClaudeProvider(cliPath), nil
3838
case "codex":
3939
return NewCodexProvider(cliPath), nil
40+
case "opencode":
41+
return NewOpenCodeProvider(cliPath), nil
4042
default:
41-
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\" or \"codex\"", providerName)
43+
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", or \"opencode\"", providerName)
4244
}
4345
}
4446

internal/agent/resolve_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,35 @@ func TestResolve_normalize(t *testing.T) {
107107
}
108108
}
109109

110+
func TestResolve_opencode(t *testing.T) {
111+
// Test OpenCode provider resolution
112+
got := mustResolve(t, "opencode", "", nil)
113+
if got.Name() != "OpenCode" {
114+
t.Errorf("Resolve(opencode) name = %q, want OpenCode", got.Name())
115+
}
116+
if got.CLIPath() != "opencode" {
117+
t.Errorf("Resolve(opencode) CLIPath = %q, want opencode", got.CLIPath())
118+
}
119+
120+
// Test OpenCode with custom path
121+
got = mustResolve(t, "opencode", "/usr/local/bin/opencode", nil)
122+
if got.CLIPath() != "/usr/local/bin/opencode" {
123+
t.Errorf("Resolve(opencode, /usr/local/bin/opencode) CLIPath = %q, want /usr/local/bin/opencode", got.CLIPath())
124+
}
125+
126+
// Test from config
127+
cfg := &config.Config{}
128+
cfg.Agent.Provider = "opencode"
129+
cfg.Agent.CLIPath = "/opt/opencode"
130+
got = mustResolve(t, "", "", cfg)
131+
if got.Name() != "OpenCode" {
132+
t.Errorf("Resolve(_, _, config opencode) name = %q, want OpenCode", got.Name())
133+
}
134+
if got.CLIPath() != "/opt/opencode" {
135+
t.Errorf("Resolve(_, _, config opencode) CLIPath = %q, want /opt/opencode", got.CLIPath())
136+
}
137+
}
138+
110139
func TestResolve_unknownProvider(t *testing.T) {
111140
_, err := Resolve("typo", "", nil)
112141
if err == nil {

internal/config/config.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ type Config struct {
1616
Agent AgentConfig `yaml:"agent"`
1717
}
1818

19-
// AgentConfig holds agent CLI settings (Claude vs Codex).
19+
// AgentConfig holds agent CLI settings (Claude, Codex, or OpenCode).
2020
type AgentConfig struct {
21-
Provider string `yaml:"provider"` // "claude" (default) | "codex"
22-
CLIPath string `yaml:"cliPath"` // optional custom path to CLI binary
21+
Provider string `yaml:"provider"` // "claude" (default) | "codex" | "opencode"
22+
CLIPath string `yaml:"cliPath"` // optional custom path to CLI binary
2323
}
2424

2525
// WorktreeConfig holds worktree-related settings.

0 commit comments

Comments
 (0)