Skip to content
Merged
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
7 changes: 4 additions & 3 deletions cmd/chief/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ type TUIOptions struct {
Merge bool
Force bool
NoRetry bool
Agent string // --agent claude|codex
Agent string // --agent claude|codex|opencode|cursor
AgentPath string // --agent-path
}

Expand Down Expand Up @@ -134,7 +134,7 @@ func parseAgentFlags(args []string, startIdx int) (agentName, agentPath string,
i++
agentName = args[i]
} else {
fmt.Fprintf(os.Stderr, "Error: --agent requires a value (claude, codex, or opencode)\n")
fmt.Fprintf(os.Stderr, "Error: --agent requires a value (claude, codex, opencode, or cursor)\n")
os.Exit(1)
}
case strings.HasPrefix(arg, "--agent="):
Expand Down Expand Up @@ -514,7 +514,7 @@ Commands:
help Show this help message

Global Options:
--agent <provider> Agent CLI to use: claude (default), codex, or opencode
--agent <provider> Agent CLI to use: claude (default), codex, opencode, or cursor
--agent-path <path> Custom path to agent CLI binary
--max-iterations N, -n N Set maximum iterations (default: dynamic)
--no-retry Disable auto-retry on agent crashes
Expand All @@ -541,6 +541,7 @@ Examples:
Launch auth PRD with 5 max iterations
chief --verbose Launch with raw agent output visible
chief --agent codex Use Codex CLI instead of Claude
chief --agent cursor Use Cursor CLI as agent
chief new Create PRD in .chief/prds/main/
chief new auth Create PRD in .chief/prds/auth/
chief new auth "JWT authentication for REST API"
Expand Down
1 change: 1 addition & 0 deletions docs/.vitepress/theme/components/AgentSupport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const agents = [
{ name: 'Claude Code', description: 'By Anthropic' },
{ name: 'Codex CLI', description: 'By OpenAI' },
{ name: 'OpenCode', description: 'Open source' },
{ name: 'Cursor CLI', description: 'By Cursor' },
]
</script>

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/prd-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Each PRD lives in its own subdirectory inside `.chief/prds/`:

- **`prd.md`** — Written by you, read and updated by Chief. Contains project context and structured user stories.
- **`progress.md`** — Written by the agent. Tracks what was done, what changed, and what was learned.
- **`claude.log`** (or `codex.log` / `opencode.log`) — Written by Chief. Raw output from the agent for debugging.
- **`claude.log`** (or `codex.log` / `opencode.log` / `cursor.log`) — Written by Chief. Raw output from the agent for debugging.

## prd.md — The PRD File

Expand Down
9 changes: 9 additions & 0 deletions docs/guide/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ To use [OpenCode CLI](https://opencode.ai) as an alternative:
2. Ensure `opencode` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml` (see [Configuration](/reference/configuration#agent)).
3. Run Chief with `chief --agent opencode` or set `CHIEF_AGENT=opencode`, or set `agent.provider: opencode` in `.chief/config.yaml`.

### Option D: Cursor CLI

To use [Cursor CLI](https://cursor.com/docs/cli/overview) as the agent:

1. Install Cursor CLI per the [official docs](https://cursor.com/docs/cli/overview)
2. Ensure `agent` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml`.
3. Run `agent login` for authentication.
4. Run Chief with `chief --agent cursor` or set `CHIEF_AGENT=cursor`, or set `agent.provider: cursor` in `.chief/config.yaml`.

### Optional: GitHub CLI (`gh`)

If you want Chief to automatically create pull requests when a PRD completes, install the [GitHub CLI](https://cli.github.com/):
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ Before you begin, make sure you have:
- [Claude Code](https://github.com/anthropics/claude-code) (default)
- [Codex CLI](https://developers.openai.com/codex/cli/reference)
- [OpenCode CLI](https://opencode.ai/docs/)
- [Cursor CLI](https://cursor.com/docs/cli/overview)
- A project you want to work on (or create a new one)

::: tip Verify your agent CLI is working
Run the version command for your agent to confirm it's installed:
- `claude --version` (Claude Code)
- `codex --version` (Codex)
- `opencode --version` (OpenCode)
- `agent --version` (Cursor CLI)
:::

## Step 1: Install Chief
Expand Down
17 changes: 13 additions & 4 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Chief stores project-level settings in `.chief/config.yaml`. This file is create

```yaml
agent:
provider: claude # or "codex" or "opencode"
provider: claude # or "codex", "opencode", or "cursor"
cliPath: "" # optional path to CLI binary
worktree:
setup: "npm install"
Expand All @@ -27,7 +27,7 @@ onComplete:

| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude`, `codex`, or `opencode` |
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude`, `codex`, `opencode`, or `cursor` |
| `agent.cliPath` | string | `""` | Optional path to the agent binary (e.g. `/usr/local/bin/opencode`). If empty, Chief uses the provider name from PATH. |
| `worktree.setup` | string | `""` | Shell command to run in new worktrees (e.g., `npm install`, `go mod download`) |
| `onComplete.push` | bool | `false` | Automatically push the branch to remote when a PRD completes |
Expand Down Expand Up @@ -88,7 +88,7 @@ These settings are saved to `.chief/config.yaml` and can be changed at any time

| Flag | Description | Default |
|------|-------------|---------|
| `--agent <provider>` | Agent CLI to use: `claude`, `codex`, or `opencode` | From config / env / `claude` |
| `--agent <provider>` | Agent CLI to use: `claude`, `codex`, `opencode`, or `cursor` | From config / env / `claude` |
| `--agent-path <path>` | Custom path to the agent CLI binary | From config / env |
| `--max-iterations <n>`, `-n` | Loop iteration limit | Dynamic |
| `--no-retry` | Disable auto-retry on agent crashes | `false` |
Expand All @@ -100,7 +100,7 @@ When `--max-iterations` is not specified, Chief calculates a dynamic limit based

## Agent

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

- **Config:** `agent.provider: opencode` and optionally `agent.cliPath: /path/to/opencode` in `.chief/config.yaml`
- **Environment:** `CHIEF_AGENT=opencode`, `CHIEF_AGENT_PATH=/path/to/opencode`
Expand All @@ -120,6 +120,15 @@ claude config set model claude-3-opus-20240229

See [Claude Code documentation](https://github.com/anthropics/claude-code) for details.

When using Cursor CLI:

```bash
# Authentication (or set CURSOR_API_KEY for headless)
agent login
```

Chief runs Cursor in headless mode with `--trust` and `--force` so it can modify files without prompts. See [Cursor CLI documentation](https://cursor.com/docs/cli/overview) for details.

## Permission Handling

Some agents (like Claude Code) ask for permission before executing bash commands, writing files, and making network requests. Chief automatically configures the agent for autonomous operation by disabling these prompts.
Expand Down
17 changes: 12 additions & 5 deletions docs/troubleshooting/common-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ Error: OpenCode CLI not found in PATH. Install it or set agent.cliPath in .chief
cliPath: /usr/local/bin/opencode
```
Verify with `opencode --version` (or your `cliPath`).
- **Cursor:** Install [Cursor CLI](https://cursor.com/docs/cli/overview) and ensure `agent` is in PATH, or set the path in config:
```yaml
agent:
provider: cursor
cliPath: /path/to/agent
```
Run `agent login`. Verify with `agent --version` (or your `cliPath`).

## Permission Denied

Expand All @@ -63,9 +70,9 @@ Chief automatically configures the agent for autonomous operation by disabling p

**Solution:**

1. Check the agent log for errors (the log file matches your agent: `claude.log`, `codex.log`, or `opencode.log`):
1. Check the agent log for errors (the log file matches your agent: `claude.log`, `codex.log`, `opencode.log`, or `cursor.log`):
```bash
tail -100 .chief/prds/your-prd/claude.log # or codex.log / opencode.log
tail -100 .chief/prds/your-prd/claude.log # or codex.log / opencode.log / cursor.log
```

2. Manually mark story complete if appropriate by editing `prd.md`:
Expand All @@ -85,7 +92,7 @@ Chief automatically configures the agent for autonomous operation by disabling p

1. Check the agent log for what the agent is doing:
```bash
tail -f .chief/prds/your-prd/claude.log # or codex.log / opencode.log
tail -f .chief/prds/your-prd/claude.log # or codex.log / opencode.log / cursor.log
```

2. Simplify the current story's acceptance criteria
Expand Down Expand Up @@ -114,7 +121,7 @@ Chief automatically configures the agent for autonomous operation by disabling p

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

## "No PRD Found"
Expand Down Expand Up @@ -255,4 +262,4 @@ If none of these solutions help:
3. Open a new issue with:
- Chief version (`chief --version`)
- Your `prd.md` (sanitized)
- Relevant agent log excerpts (e.g. `claude.log`, `codex.log`, or `opencode.log`)
- Relevant agent log excerpts (e.g. `claude.log`, `codex.log`, `opencode.log`, or `cursor.log`)
95 changes: 95 additions & 0 deletions internal/agent/cursor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package agent

import (
"context"
"encoding/json"
"os/exec"
"strings"

"github.com/minicodemonkey/chief/internal/loop"
)

// CursorProvider implements loop.Provider for the Cursor CLI (agent).
type CursorProvider struct {
cliPath string
}

// NewCursorProvider returns a Provider for the Cursor CLI.
// If cliPath is empty, "agent" is used.
func NewCursorProvider(cliPath string) *CursorProvider {
if cliPath == "" {
cliPath = "agent"
}
return &CursorProvider{cliPath: cliPath}
}

// Name implements loop.Provider.
func (p *CursorProvider) Name() string { return "Cursor" }

// CLIPath implements loop.Provider.
func (p *CursorProvider) CLIPath() string { return p.cliPath }

// LoopCommand implements loop.Provider.
// Prompt is supplied via stdin; Cursor CLI reads it when -p has no argument.
func (p *CursorProvider) LoopCommand(ctx context.Context, prompt, workDir string) *exec.Cmd {
cmd := exec.CommandContext(ctx, p.cliPath,
"-p",
"--output-format", "stream-json",
"--force",
"--workspace", workDir,
"--trust",
)
cmd.Dir = workDir
cmd.Stdin = strings.NewReader(prompt)
return cmd
}

// InteractiveCommand implements loop.Provider.
func (p *CursorProvider) InteractiveCommand(workDir, prompt string) *exec.Cmd {
cmd := exec.Command(p.cliPath, prompt)
cmd.Dir = workDir
return cmd
}

// ParseLine implements loop.Provider.
func (p *CursorProvider) ParseLine(line string) *loop.Event {
return loop.ParseLineCursor(line)
}

// LogFileName implements loop.Provider.
func (p *CursorProvider) LogFileName() string { return "cursor.log" }

// cursorResultLine is the structure for Cursor's result/success JSON lines.
type cursorResultLine struct {
Type string `json:"type"`
Subtype string `json:"subtype,omitempty"`
Result string `json:"result,omitempty"`
}

// CleanOutput extracts the result from Cursor's json or stream-json output.
// For stream-json, finds the last type "result", subtype "success" and returns its result field.
// For single-line json, parses and returns result.
func (p *CursorProvider) CleanOutput(output string) string {
output = strings.TrimSpace(output)
if output == "" {
return output
}
// Try single JSON object (json output format)
var single cursorResultLine
if json.Unmarshal([]byte(output), &single) == nil && single.Type == "result" && single.Subtype == "success" && single.Result != "" {
return single.Result
}
// NDJSON: find last result/success line
lines := strings.Split(output, "\n")
for i := len(lines) - 1; i >= 0; i-- {
line := strings.TrimSpace(lines[i])
if line == "" {
continue
}
var ev cursorResultLine
if json.Unmarshal([]byte(line), &ev) == nil && ev.Type == "result" && ev.Subtype == "success" && ev.Result != "" {
return ev.Result
}
}
return output
}
101 changes: 101 additions & 0 deletions internal/agent/cursor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package agent

import (
"context"
"testing"

"github.com/minicodemonkey/chief/internal/loop"
)

func TestCursorProvider_Name(t *testing.T) {
p := NewCursorProvider("")
if p.Name() != "Cursor" {
t.Errorf("Name() = %q, want Cursor", p.Name())
}
}

func TestCursorProvider_CLIPath(t *testing.T) {
p := NewCursorProvider("")
if p.CLIPath() != "agent" {
t.Errorf("CLIPath() empty arg = %q, want agent", p.CLIPath())
}
p2 := NewCursorProvider("/usr/local/bin/agent")
if p2.CLIPath() != "/usr/local/bin/agent" {
t.Errorf("CLIPath() custom = %q, want /usr/local/bin/agent", p2.CLIPath())
}
}

func TestCursorProvider_LogFileName(t *testing.T) {
p := NewCursorProvider("")
if p.LogFileName() != "cursor.log" {
t.Errorf("LogFileName() = %q, want cursor.log", p.LogFileName())
}
}

func TestCursorProvider_LoopCommand(t *testing.T) {
ctx := context.Background()
p := NewCursorProvider("/bin/agent")
cmd := p.LoopCommand(ctx, "hello world", "/work/dir")

if cmd.Path != "/bin/agent" {
t.Errorf("LoopCommand Path = %q, want /bin/agent", cmd.Path)
}
wantArgs := []string{"/bin/agent", "-p", "--output-format", "stream-json", "--force", "--workspace", "/work/dir", "--trust"}
if len(cmd.Args) != len(wantArgs) {
t.Fatalf("LoopCommand Args len = %d, want %d: %v", len(cmd.Args), len(wantArgs), cmd.Args)
}
for i, w := range wantArgs {
if cmd.Args[i] != w {
t.Errorf("LoopCommand Args[%d] = %q, want %q", i, cmd.Args[i], w)
}
}
if cmd.Dir != "/work/dir" {
t.Errorf("LoopCommand Dir = %q, want /work/dir", cmd.Dir)
}
if cmd.Stdin == nil {
t.Error("LoopCommand Stdin must be set (prompt via stdin)")
}
}

func TestCursorProvider_InteractiveCommand(t *testing.T) {
p := NewCursorProvider("/bin/agent")
cmd := p.InteractiveCommand("/work", "my prompt")
if cmd.Dir != "/work" {
t.Errorf("InteractiveCommand Dir = %q, want /work", cmd.Dir)
}
if len(cmd.Args) != 2 || cmd.Args[0] != "/bin/agent" || cmd.Args[1] != "my prompt" {
t.Errorf("InteractiveCommand Args = %v, want [/bin/agent my prompt]", cmd.Args)
}
}

func TestCursorProvider_ParseLine(t *testing.T) {
p := NewCursorProvider("")
line := `{"type":"system","subtype":"init","session_id":"x"}`
e := p.ParseLine(line)
if e == nil {
t.Fatal("ParseLine(system init) returned nil")
}
if e.Type != loop.EventIterationStart {
t.Errorf("ParseLine(system init) Type = %v, want EventIterationStart", e.Type)
}
}

func TestCursorProvider_CleanOutput(t *testing.T) {
p := NewCursorProvider("")
// NDJSON: last result/success
ndjson := `{"type":"system","subtype":"init"}
{"type":"result","subtype":"success","result":"final answer","session_id":"x"}`
if got := p.CleanOutput(ndjson); got != "final answer" {
t.Errorf("CleanOutput(NDJSON) = %q, want final answer", got)
}
// Single JSON result
single := `{"type":"result","subtype":"success","result":"single result","session_id":"x"}`
if got := p.CleanOutput(single); got != "single result" {
t.Errorf("CleanOutput(single JSON) = %q, want single result", got)
}
// No result: return as-is
plain := "plain text"
if got := p.CleanOutput(plain); got != plain {
t.Errorf("CleanOutput(plain) = %q, want %q", got, plain)
}
}
4 changes: 3 additions & 1 deletion internal/agent/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ func Resolve(flagAgent, flagPath string, cfg *config.Config) (loop.Provider, err
return NewCodexProvider(cliPath), nil
case "opencode":
return NewOpenCodeProvider(cliPath), nil
case "cursor":
return NewCursorProvider(cliPath), nil
default:
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", or \"opencode\"", providerName)
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", \"opencode\", or \"cursor\"", providerName)
}
}

Expand Down
Loading