Skip to content

Commit 0e15001

Browse files
Merge pull request #44 from MiniCodeMonkey/feature/opencode-support-v2
fix(agent): review fixes for Codex and OpenCode support
2 parents 56dd9e6 + 99ace90 commit 0e15001

44 files changed

Lines changed: 2239 additions & 386 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,26 @@ 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) 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
48+
49+
Use Claude by default, or configure Codex or OpenCode in `.chief/config.yaml`:
50+
51+
```yaml
52+
agent:
53+
provider: opencode
54+
cliPath: /usr/local/bin/opencode # optional
55+
```
56+
57+
Or run with `chief --agent opencode` or set `CHIEF_AGENT=opencode`.
4858

4959
## License
5060

5161
MIT
5262

5363
## Acknowledgments
5464

65+
- [@Simon-BEE](https://github.com/Simon-BEE) — Multi-agent architecture and Codex CLI integration
66+
- [@tpaulshippy](https://github.com/tpaulshippy) — OpenCode CLI support and NDJSON parser
5567
- [snarktank/ralph](https://github.com/snarktank/ralph) — The original Ralph implementation that inspired this project
5668
- [Geoffrey Huntley](https://ghuntley.com/ralph/) — For coining the "Ralph Wiggum loop" pattern
5769
- [Bubble Tea](https://github.com/charmbracelet/bubbletea) — TUI framework

cmd/chief/main.go

Lines changed: 113 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import (
88
"strings"
99

1010
tea "github.com/charmbracelet/bubbletea"
11+
"github.com/minicodemonkey/chief/internal/agent"
1112
"github.com/minicodemonkey/chief/internal/cmd"
1213
"github.com/minicodemonkey/chief/internal/config"
1314
"github.com/minicodemonkey/chief/internal/git"
15+
"github.com/minicodemonkey/chief/internal/loop"
1416
"github.com/minicodemonkey/chief/internal/prd"
1517
"github.com/minicodemonkey/chief/internal/tui"
1618
)
@@ -26,6 +28,8 @@ type TUIOptions struct {
2628
Merge bool
2729
Force bool
2830
NoRetry bool
31+
Agent string // --agent claude|codex
32+
AgentPath string // --agent-path
2933
}
3034

3135
func main() {
@@ -118,6 +122,40 @@ func listAvailablePRDs() []string {
118122
return names
119123
}
120124

125+
// parseAgentFlags extracts --agent and --agent-path from args[startIdx:],
126+
// returning the agent name, agent path, remaining args (with agent flags removed),
127+
// and the updated index offsets. It exits on missing values.
128+
func parseAgentFlags(args []string, startIdx int) (agentName, agentPath string, remaining []string) {
129+
for i := startIdx; i < len(args); i++ {
130+
arg := args[i]
131+
switch {
132+
case arg == "--agent":
133+
if i+1 < len(args) {
134+
i++
135+
agentName = args[i]
136+
} else {
137+
fmt.Fprintf(os.Stderr, "Error: --agent requires a value (claude, codex, or opencode)\n")
138+
os.Exit(1)
139+
}
140+
case strings.HasPrefix(arg, "--agent="):
141+
agentName = strings.TrimPrefix(arg, "--agent=")
142+
case arg == "--agent-path":
143+
if i+1 < len(args) {
144+
i++
145+
agentPath = args[i]
146+
} else {
147+
fmt.Fprintf(os.Stderr, "Error: --agent-path requires a value\n")
148+
os.Exit(1)
149+
}
150+
case strings.HasPrefix(arg, "--agent-path="):
151+
agentPath = strings.TrimPrefix(arg, "--agent-path=")
152+
default:
153+
remaining = append(remaining, arg)
154+
}
155+
}
156+
return
157+
}
158+
121159
// parseTUIFlags parses command-line flags for TUI mode
122160
func parseTUIFlags() *TUIOptions {
123161
opts := &TUIOptions{
@@ -129,6 +167,9 @@ func parseTUIFlags() *TUIOptions {
129167
NoRetry: false,
130168
}
131169

170+
// Pre-extract agent flags so they don't interfere with positional arg parsing
171+
opts.Agent, opts.AgentPath, _ = parseAgentFlags(os.Args, 1)
172+
132173
for i := 1; i < len(os.Args); i++ {
133174
arg := os.Args[i]
134175

@@ -147,6 +188,10 @@ func parseTUIFlags() *TUIOptions {
147188
opts.Force = true
148189
case arg == "--no-retry":
149190
opts.NoRetry = true
191+
case arg == "--agent" || arg == "--agent-path":
192+
i++ // skip value (already parsed by parseAgentFlags)
193+
case strings.HasPrefix(arg, "--agent=") || strings.HasPrefix(arg, "--agent-path="):
194+
// already parsed by parseAgentFlags
150195
case arg == "--max-iterations" || arg == "-n":
151196
// Next argument should be the number
152197
if i+1 < len(os.Args) {
@@ -211,14 +256,23 @@ func parseTUIFlags() *TUIOptions {
211256
func runNew() {
212257
opts := cmd.NewOptions{}
213258

214-
// Parse arguments: chief new [name] [context...]
215-
if len(os.Args) > 2 {
216-
opts.Name = os.Args[2]
259+
// Parse arguments: chief new [name] [context...] [--agent X] [--agent-path X]
260+
flagAgent, flagPath, positional := parseAgentFlags(os.Args, 2)
261+
// Filter out remaining flags, keep only positional args
262+
var args []string
263+
for _, a := range positional {
264+
if !strings.HasPrefix(a, "-") {
265+
args = append(args, a)
266+
}
267+
}
268+
if len(args) > 0 {
269+
opts.Name = args[0]
217270
}
218-
if len(os.Args) > 3 {
219-
opts.Context = strings.Join(os.Args[3:], " ")
271+
if len(args) > 1 {
272+
opts.Context = strings.Join(args[1:], " ")
220273
}
221274

275+
opts.Provider = resolveProvider(flagAgent, flagPath)
222276
if err := cmd.RunNew(opts); err != nil {
223277
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
224278
os.Exit(1)
@@ -228,22 +282,22 @@ func runNew() {
228282
func runEdit() {
229283
opts := cmd.EditOptions{}
230284

231-
// Parse arguments: chief edit [name] [--merge] [--force]
232-
for i := 2; i < len(os.Args); i++ {
233-
arg := os.Args[i]
234-
switch arg {
235-
case "--merge":
285+
// Parse arguments: chief edit [name] [--merge] [--force] [--agent X] [--agent-path X]
286+
flagAgent, flagPath, remaining := parseAgentFlags(os.Args, 2)
287+
for _, arg := range remaining {
288+
switch {
289+
case arg == "--merge":
236290
opts.Merge = true
237-
case "--force":
291+
case arg == "--force":
238292
opts.Force = true
239293
default:
240-
// If not a flag, treat as PRD name (first non-flag arg)
241294
if opts.Name == "" && !strings.HasPrefix(arg, "-") {
242295
opts.Name = arg
243296
}
244297
}
245298
}
246299

300+
opts.Provider = resolveProvider(flagAgent, flagPath)
247301
if err := cmd.RunEdit(opts); err != nil {
248302
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
249303
os.Exit(1)
@@ -282,7 +336,33 @@ func runList() {
282336
}
283337
}
284338

339+
// resolveProvider loads config and resolves the agent provider, exiting on error.
340+
func resolveProvider(flagAgent, flagPath string) loop.Provider {
341+
cwd, err := os.Getwd()
342+
if err != nil {
343+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
344+
os.Exit(1)
345+
}
346+
cfg, err := config.Load(cwd)
347+
if err != nil {
348+
fmt.Fprintf(os.Stderr, "Error: failed to load .chief/config.yaml: %v\n", err)
349+
os.Exit(1)
350+
}
351+
provider, err := agent.Resolve(flagAgent, flagPath, cfg)
352+
if err != nil {
353+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
354+
os.Exit(1)
355+
}
356+
if err := agent.CheckInstalled(provider); err != nil {
357+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
358+
os.Exit(1)
359+
}
360+
return provider
361+
}
362+
285363
func runTUIWithOptions(opts *TUIOptions) {
364+
provider := resolveProvider(opts.Agent, opts.AgentPath)
365+
286366
prdPath := opts.PRDPath
287367

288368
// If no PRD specified, try to find one
@@ -322,7 +402,8 @@ func runTUIWithOptions(opts *TUIOptions) {
322402

323403
// Create the PRD
324404
newOpts := cmd.NewOptions{
325-
Name: result.PRDName,
405+
Name: result.PRDName,
406+
Provider: provider,
326407
}
327408
if err := cmd.RunNew(newOpts); err != nil {
328409
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -344,19 +425,19 @@ func runTUIWithOptions(opts *TUIOptions) {
344425
fmt.Printf("Warning: failed to check conversion status: %v\n", err)
345426
} else if needsConvert {
346427
fmt.Println("prd.md is newer than prd.json, running conversion...")
347-
convertOpts := prd.ConvertOptions{
348-
PRDDir: prdDir,
349-
Merge: opts.Merge,
350-
Force: opts.Force,
351-
}
352-
if err := prd.Convert(convertOpts); err != nil {
428+
if err := cmd.RunConvertWithOptions(cmd.ConvertOptions{
429+
PRDDir: prdDir,
430+
Merge: opts.Merge,
431+
Force: opts.Force,
432+
Provider: provider,
433+
}); err != nil {
353434
fmt.Printf("Error converting PRD: %v\n", err)
354435
os.Exit(1)
355436
}
356437
fmt.Println("Conversion complete.")
357438
}
358439

359-
app, err := tui.NewAppWithOptions(prdPath, opts.MaxIterations)
440+
app, err := tui.NewAppWithOptions(prdPath, opts.MaxIterations, provider)
360441
if err != nil {
361442
// Check if this is a missing PRD file error
362443
if os.IsNotExist(err) || strings.Contains(err.Error(), "no such file") {
@@ -403,7 +484,8 @@ func runTUIWithOptions(opts *TUIOptions) {
403484
case tui.PostExitInit:
404485
// Run new command then restart TUI
405486
newOpts := cmd.NewOptions{
406-
Name: finalApp.PostExitPRD,
487+
Name: finalApp.PostExitPRD,
488+
Provider: provider,
407489
}
408490
if err := cmd.RunNew(newOpts); err != nil {
409491
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -416,9 +498,10 @@ func runTUIWithOptions(opts *TUIOptions) {
416498
case tui.PostExitEdit:
417499
// Run edit command then restart TUI
418500
editOpts := cmd.EditOptions{
419-
Name: finalApp.PostExitPRD,
420-
Merge: opts.Merge,
421-
Force: opts.Force,
501+
Name: finalApp.PostExitPRD,
502+
Merge: opts.Merge,
503+
Force: opts.Force,
504+
Provider: provider,
422505
}
423506
if err := cmd.RunEdit(editOpts); err != nil {
424507
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -447,9 +530,11 @@ Commands:
447530
help Show this help message
448531
449532
Global Options:
533+
--agent <provider> Agent CLI to use: claude (default), codex, or opencode
534+
--agent-path <path> Custom path to agent CLI binary
450535
--max-iterations N, -n N Set maximum iterations (default: dynamic)
451-
--no-retry Disable auto-retry on Claude crashes
452-
--verbose Show raw Claude output in log
536+
--no-retry Disable auto-retry on agent crashes
537+
--verbose Show raw agent output in log
453538
--merge Auto-merge progress on conversion conflicts
454539
--force Auto-overwrite on conversion conflicts
455540
--help, -h Show this help message
@@ -470,7 +555,8 @@ Examples:
470555
chief -n 20 Launch with 20 max iterations
471556
chief --max-iterations=5 auth
472557
Launch auth PRD with 5 max iterations
473-
chief --verbose Launch with raw Claude output visible
558+
chief --verbose Launch with raw agent output visible
559+
chief --agent codex Use Codex CLI instead of Claude
474560
chief new Create PRD in .chief/prds/main/
475561
chief new auth Create PRD in .chief/prds/auth/
476562
chief new auth "JWT authentication for REST API"

docs/guide/installation.md

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

99
## Prerequisites
1010

11-
Before installing Chief, ensure you have **Claude Code CLI** installed and authenticated:
11+
Chief needs an agent CLI: **Claude Code** (default), **Codex**, or **OpenCode**. Install at least one and authenticate.
12+
13+
### Option A: Claude Code CLI (default)
1214

1315
::: code-group
1416

@@ -27,8 +29,32 @@ npx @anthropic-ai/claude-code login
2729

2830
:::
2931

30-
::: tip Verify Claude Code Installation
31-
Run `claude --version` to confirm Claude Code is installed. Chief will not work without it.
32+
::: tip Verify Claude Code
33+
Run `claude --version` to confirm Claude Code is installed.
34+
:::
35+
36+
### Option B: Codex CLI
37+
38+
To use [OpenAI Codex CLI](https://developers.openai.com/codex/cli/reference) instead of Claude:
39+
40+
1. Install Codex per the [official reference](https://developers.openai.com/codex/cli/reference).
41+
2. Ensure `codex` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml` (see [Configuration](/reference/configuration#agent)).
42+
3. Run Chief with `chief --agent codex` or set `CHIEF_AGENT=codex`, or set `agent.provider: codex` in `.chief/config.yaml`.
43+
44+
::: tip Verify Codex
45+
Run `codex --version` (or your custom path) to confirm Codex is available.
46+
:::
47+
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.
3258
:::
3359

3460
### Optional: GitHub CLI (`gh`)
@@ -238,11 +264,12 @@ chief --version
238264
# View help
239265
chief --help
240266

241-
# Check that Claude Code is accessible
267+
# Check that your agent CLI is accessible (Claude default, or codex if configured)
242268
claude --version
269+
# or: codex --version
243270
```
244271

245-
Expected output:
272+
Expected output (example with Claude):
246273

247274
```
248275
$ chief --version

0 commit comments

Comments
 (0)