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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ See the [documentation](https://minicodemonkey.github.io/chief/concepts/how-it-w

## Requirements

- **[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)**, **[Codex CLI](https://developers.openai.com/codex/cli/reference)**, **[OpenCode CLI](https://opencode.ai)**, or **[Gemini CLI](https://github.com/google-gemini/gemini-cli)** installed and authenticated
- **[Claude Code CLI](https://docs.anthropic.com/en/docs/claude-code)**, **[Codex CLI](https://developers.openai.com/codex/cli/reference)**, **[OpenCode CLI](https://opencode.ai)**, **[Gemini CLI](https://github.com/google-gemini/gemini-cli)**, or **[GitHub Copilot CLI](https://github.com/features/copilot/cli)** installed and authenticated

Use Claude by default, or configure another provider in `.chief/config.yaml`:

Expand All @@ -56,7 +56,7 @@ agent:

Or run with `chief --agent opencode` or set `CHIEF_AGENT=opencode`.

Supported values for `provider` are: `claude`, `codex`, `opencode`, `cursor`, and `gemini`.
Supported values for `provider` are: `claude`, `codex`, `opencode`, `cursor`, `gemini`, and `copilot`.

## License

Expand Down
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|opencode|cursor
Agent string // --agent claude|codex|opencode|cursor|gemini|copilot
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, opencode, cursor or gemini)\n")
fmt.Fprintf(os.Stderr, "Error: --agent requires a value (claude, codex, opencode, cursor, gemini or copilot)\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, opencode, cursor, or gemini
--agent <provider> Agent CLI to use: claude (default), codex, opencode, cursor, gemini, or copilot
--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 Down Expand Up @@ -542,6 +542,7 @@ Examples:
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 --agent copilot Use GitHub Copilot 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
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` / `cursor.log`) — Written by Chief. Raw output from the agent for debugging.
- **Agent log** (`claude.log`, `codex.log`, `opencode.log`, `cursor.log`, `gemini.log`, or `copilot.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 @@ -54,6 +54,15 @@ To use [Cursor CLI](https://cursor.com/docs/cli/overview) as the agent:
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`.

### Option E: GitHub Copilot CLI

To use [GitHub Copilot CLI](https://github.com/features/copilot/cli) as the agent:

1. Install Copilot CLI using the [official instructions](https://docs.github.com/en/copilot/how-tos/copilot-cli/set-up-copilot-cli/install-copilot-cli).
2. Ensure `copilot` is on your PATH, or set `agent.cliPath` in `.chief/config.yaml`.
3. Run `copilot login` for authentication.
4. Run Chief with `chief --agent copilot` or set `CHIEF_AGENT=copilot`, or set `agent.provider: copilot` 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
4 changes: 4 additions & 0 deletions docs/guide/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Before you begin, make sure you have:
- [Codex CLI](https://developers.openai.com/codex/cli/reference)
- [OpenCode CLI](https://opencode.ai/docs/)
- [Cursor CLI](https://cursor.com/docs/cli/overview)
- [Gemini CLI](https://github.com/google-gemini/gemini-cli)
- [GitHub Copilot CLI](https://github.com/features/copilot/cli)
- A project you want to work on (or create a new one)

::: tip Verify your agent CLI is working
Expand All @@ -27,6 +29,8 @@ Run the version command for your agent to confirm it's installed:
- `codex --version` (Codex)
- `opencode --version` (OpenCode)
- `agent --version` (Cursor CLI)
- `gemini --version` (Gemini CLI)
- `copilot --version` (GitHub Copilot CLI)
:::

## Step 1: Install Chief
Expand Down
8 changes: 4 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", "opencode", or "cursor"
provider: claude # or "codex", "opencode", "cursor", "gemini", or "copilot"
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`, `opencode`, or `cursor` |
| `agent.provider` | string | `"claude"` | Agent CLI to use: `claude`, `codex`, `opencode`, `cursor`, `gemini`, or `copilot` |
| `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`, `opencode`, or `cursor` | From config / env / `claude` |
| `--agent <provider>` | Agent CLI to use: `claude`, `codex`, `opencode`, `cursor`, `gemini`, or `copilot` | 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**, **OpenCode CLI**, **Cursor CLI**, or **Gemini CLI** as the agent. Choose via:
Chief can use **Claude Code** (default), **Codex CLI**, **OpenCode CLI**, **Cursor CLI**, **Gemini CLI**, or **GitHub Copilot 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 Down
17 changes: 12 additions & 5 deletions docs/troubleshooting/common-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ Error: OpenCode CLI not found in PATH. Install it or set agent.cliPath in .chief
cliPath: /path/to/agent
```
Run `agent login`. Verify with `agent --version` (or your `cliPath`).
- **GitHub Copilot:** Install [GitHub Copilot CLI](https://github.com/features/copilot/cli) and ensure `copilot` is in PATH, or set the path in config:
```yaml
agent:
provider: copilot
cliPath: /usr/local/bin/copilot
```
Run `copilot login`. Verify with `copilot --version` (or your `cliPath`).

## Permission Denied

Expand All @@ -70,9 +77,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`, `opencode.log`, or `cursor.log`):
1. Check the agent log for errors (the log file matches your agent: `claude.log`, `codex.log`, `opencode.log`, `cursor.log`, `gemini.log`, or `copilot.log`):
```bash
tail -100 .chief/prds/your-prd/claude.log # or codex.log / opencode.log / cursor.log
tail -100 .chief/prds/your-prd/copilot.log
```

2. Manually mark story complete if appropriate by editing `prd.md`:
Expand All @@ -92,7 +99,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 / cursor.log
tail -f .chief/prds/your-prd/copilot.log
```

2. Simplify the current story's acceptance criteria
Expand Down Expand Up @@ -121,7 +128,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`, `opencode.log`, or `cursor.log`)
- Stuck in a loop? Check the matching agent log (for example, `claude.log` or `copilot.log`)
- Unclear acceptance criteria? Clarify them

## "No PRD Found"
Expand Down Expand Up @@ -262,4 +269,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`, `opencode.log`, or `cursor.log`)
- Relevant agent log excerpts (for example, `claude.log` or `copilot.log`)
88 changes: 88 additions & 0 deletions internal/agent/copilot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package agent

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

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

// CopilotProvider implements loop.Provider for GitHub Copilot CLI.
type CopilotProvider struct {
cliPath string
}

// NewCopilotProvider returns a Provider for GitHub Copilot CLI.
// If cliPath is empty, "copilot" is used.
func NewCopilotProvider(cliPath string) *CopilotProvider {
if cliPath == "" {
cliPath = "copilot"
}
return &CopilotProvider{cliPath: cliPath}
}

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

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

// LoopCommand implements loop.Provider.
func (p *CopilotProvider) LoopCommand(ctx context.Context, prompt, workDir string) *exec.Cmd {
cmd := exec.CommandContext(ctx, p.cliPath,
"-p", prompt,
"--output-format", "json",
"--allow-all",
"--no-ask-user",
"--no-color",
"--no-auto-update",
"--no-remote",
"--no-remote-export",
)
cmd.Dir = workDir
return cmd
}

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

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

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

// CleanOutput extracts complete assistant messages from Copilot's JSONL output.
func (p *CopilotProvider) CleanOutput(output string) string {
output = strings.TrimSpace(output)
if output == "" {
return output
}

var messages []string
for _, line := range strings.Split(output, "\n") {
var event struct {
Type string `json:"type"`
Data struct {
Content string `json:"content"`
} `json:"data"`
}
if json.Unmarshal([]byte(line), &event) == nil &&
event.Type == "assistant.message" &&
event.Data.Content != "" {
messages = append(messages, event.Data.Content)
}
}
if len(messages) > 0 {
return strings.Join(messages, "\n")
}
return output
}
82 changes: 82 additions & 0 deletions internal/agent/copilot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package agent

import (
"context"
"strings"
"testing"

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

func TestCopilotProvider(t *testing.T) {
p := NewCopilotProvider("")
if p.Name() != "Copilot" {
t.Errorf("Name() = %q, want Copilot", p.Name())
}
if p.CLIPath() != "copilot" {
t.Errorf("CLIPath() = %q, want copilot", p.CLIPath())
}
if p.LogFileName() != "copilot.log" {
t.Errorf("LogFileName() = %q, want copilot.log", p.LogFileName())
}

custom := NewCopilotProvider("/opt/copilot")
if custom.CLIPath() != "/opt/copilot" {
t.Errorf("custom CLIPath() = %q, want /opt/copilot", custom.CLIPath())
}
}

func TestCopilotProvider_LoopCommand(t *testing.T) {
p := NewCopilotProvider("/bin/copilot")
cmd := p.LoopCommand(context.Background(), "build it", "/work")

wantArgs := []string{
"/bin/copilot", "-p", "build it",
"--output-format", "json",
"--allow-all",
"--no-ask-user",
"--no-color",
"--no-auto-update",
"--no-remote",
"--no-remote-export",
}
if strings.Join(cmd.Args, "\x00") != strings.Join(wantArgs, "\x00") {
t.Errorf("LoopCommand Args = %v, want %v", cmd.Args, wantArgs)
}
if cmd.Dir != "/work" {
t.Errorf("LoopCommand Dir = %q, want /work", cmd.Dir)
}
}

func TestCopilotProvider_InteractiveCommand(t *testing.T) {
p := NewCopilotProvider("/bin/copilot")
cmd := p.InteractiveCommand("/work", "review this")
wantArgs := []string{"/bin/copilot", "-i", "review this"}
if strings.Join(cmd.Args, "\x00") != strings.Join(wantArgs, "\x00") {
t.Errorf("InteractiveCommand Args = %v, want %v", cmd.Args, wantArgs)
}
if cmd.Dir != "/work" {
t.Errorf("InteractiveCommand Dir = %q, want /work", cmd.Dir)
}
}

func TestCopilotProvider_ParseLine(t *testing.T) {
p := NewCopilotProvider("")
event := p.ParseLine(`{"type":"assistant.message","data":{"content":"done <chief-done/>"}}`)
if event == nil || event.Type != loop.EventStoryDone {
t.Fatalf("ParseLine() = %#v, want EventStoryDone", event)
}
}

func TestCopilotProvider_CleanOutput(t *testing.T) {
p := NewCopilotProvider("")
output := `{"type":"assistant.message","data":{"content":"first"}}
{"type":"tool.execution_start","data":{"toolName":"bash"}}
{"type":"assistant.message","data":{"content":"second"}}`
if got := p.CleanOutput(output); got != "first\nsecond" {
t.Errorf("CleanOutput() = %q, want %q", got, "first\nsecond")
}
if got := p.CleanOutput("plain text"); got != "plain text" {
t.Errorf("CleanOutput(plain) = %q, want plain text", got)
}
}
4 changes: 3 additions & 1 deletion internal/agent/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ func Resolve(flagAgent, flagPath string, cfg *config.Config) (loop.Provider, err
return NewCursorProvider(cliPath), nil
case "gemini":
return NewGeminiProvider(cliPath), nil
case "copilot", "copilot-cli":
return NewCopilotProvider(cliPath), nil
default:
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", \"opencode\", \"cursor\", or \"gemini\"", providerName)
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\", \"codex\", \"opencode\", \"cursor\", \"gemini\", or \"copilot\"", providerName)
}
}

Expand Down
20 changes: 20 additions & 0 deletions internal/agent/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,23 @@ func TestResolve_gemini(t *testing.T) {
}
}

func TestResolve_copilot(t *testing.T) {
for _, name := range []string{"copilot", "copilot-cli"} {
got := mustResolve(t, name, "", nil)
if got.Name() != "Copilot" {
t.Errorf("Resolve(%s) name = %q, want Copilot", name, got.Name())
}
if got.CLIPath() != "copilot" {
t.Errorf("Resolve(%s) CLIPath = %q, want copilot", name, got.CLIPath())
}
}

got := mustResolve(t, "copilot", "/usr/local/bin/copilot", nil)
if got.CLIPath() != "/usr/local/bin/copilot" {
t.Errorf("Resolve(copilot, custom path) CLIPath = %q, want /usr/local/bin/copilot", got.CLIPath())
}
}

func TestResolve_unknownProvider(t *testing.T) {
_, err := Resolve("typo", "", nil)
if err == nil {
Expand All @@ -189,6 +206,9 @@ func TestResolve_unknownProvider(t *testing.T) {
if !strings.Contains(err.Error(), "gemini") {
t.Errorf("error should mention gemini as a valid option: %v", err)
}
if !strings.Contains(err.Error(), "copilot") {
t.Errorf("error should mention copilot as a valid option: %v", err)
}
}

func TestCheckInstalled_notFound(t *testing.T) {
Expand Down
Loading