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
24 changes: 22 additions & 2 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ agent:
cliPath: "" # optional path to CLI binary
worktree:
setup: "npm install"
alwaysPrompt: false
promptBranchPattern: "^(main|master)$"
onComplete:
push: true
createPR: true
Expand All @@ -30,9 +32,13 @@ onComplete:
| `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`) |
| `worktree.alwaysPrompt` | bool | `false` | When true, Chief always prompts about creating a git worktree before starting a loop, regardless of the current branch name. Overrides `promptBranchPattern`. |
| `worktree.promptBranchPattern` | string (regex) | `"^(main\|master)$"` | Regular expression matched against the current branch name. When it matches, Chief prompts about creating a git worktree. Empty string disables matching. Ignored when `alwaysPrompt` is true. Invalid regex causes Chief to fail at startup with an error naming the offending field. Patterns use Go's RE2 syntax — lookarounds and backreferences are not supported. |
| `onComplete.push` | bool | `false` | Automatically push the branch to remote when a PRD completes |
| `onComplete.createPR` | bool | `false` | Automatically create a pull request when a PRD completes (requires `gh` CLI) |

If you write `promptBranchPattern: ""` explicitly, Chief skips branch-name matching entirely; only `alwaysPrompt` will trigger the prompt. Default regex: `^(main|master)$`. The `\|` shown in the default column above is Markdown escaping for the table separator; the actual regex value uses an unescaped `|`.

### Example Configurations

**Minimal (defaults):**
Expand All @@ -55,13 +61,27 @@ onComplete:
createPR: true
```

**Prompt for worktree on main, master, or any release branch:**

```yaml
worktree:
promptBranchPattern: "^(main|master|release/.*)$"
```

**Always prompt for a worktree:**

```yaml
worktree:
alwaysPrompt: true
```

## Settings TUI

Press `,` from any view in the TUI to open the Settings overlay. This provides an interactive way to view and edit all config values.
Press `,` from any view in the TUI to open the Settings overlay. This provides an interactive way to view and edit a subset of common config values.

Settings are organized by section:

- **Worktree** — Setup command (string, editable inline)
- **Worktree** — Setup command (string, editable inline), Always prompt for worktree (toggle), Prompt branch pattern (regex, editable inline; invalid regex is rejected with an inline error so the editor stays open)
- **On Complete** — Push to remote (toggle), Create pull request (toggle)

Changes are saved immediately to `.chief/config.yaml` on every edit.
Expand Down
4 changes: 2 additions & 2 deletions internal/agent/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,15 @@ func TestCheckInstalled_found(t *testing.T) {
func TestResolve_configFile(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, ".chief", "config.yaml")
if err := os.MkdirAll(filepath.Dir(cfgPath), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(cfgPath), 0755); err != nil {
t.Fatal(err)
}
const yamlContent = `
agent:
provider: codex
cliPath: /usr/local/bin/codex
`
if err := os.WriteFile(cfgPath, []byte(yamlContent), 0o644); err != nil {
if err := os.WriteFile(cfgPath, []byte(yamlContent), 0644); err != nil {
t.Fatal(err)
}
cfg, err := config.Load(dir)
Expand Down
68 changes: 64 additions & 4 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package config

import (
"fmt"
"os"
"path/filepath"
"regexp"

"gopkg.in/yaml.v3"
)
Expand All @@ -14,6 +16,8 @@ type Config struct {
Worktree WorktreeConfig `yaml:"worktree"`
OnComplete OnCompleteConfig `yaml:"onComplete"`
Agent AgentConfig `yaml:"agent"`

promptBranchRegex *regexp.Regexp
}

// AgentConfig holds agent CLI settings (Claude, Codex, OpenCode, or Cursor).
Expand All @@ -24,7 +28,9 @@ type AgentConfig struct {

// WorktreeConfig holds worktree-related settings.
type WorktreeConfig struct {
Setup string `yaml:"setup"`
Setup string `yaml:"setup"`
AlwaysPrompt bool `yaml:"alwaysPrompt"`
PromptBranchPattern string `yaml:"promptBranchPattern"`
}

// OnCompleteConfig holds post-completion automation settings.
Expand All @@ -35,7 +41,54 @@ type OnCompleteConfig struct {

// Default returns a Config with zero-value defaults.
func Default() *Config {
return &Config{}
cfg := &Config{
Worktree: WorktreeConfig{
PromptBranchPattern: "^(main|master)$",
},
}
if err := cfg.Validate(); err != nil {
panic(fmt.Sprintf("config: default config failed to validate: %v", err))
}
return cfg
}

// Validate compiles derived config state (e.g., the prompt-branch regex
// cache) and reports configuration errors. Idempotent — safe to call
// multiple times. Callers must call Validate after mutating Config fields
// that affect derived state.
func (c *Config) Validate() error {
return c.compilePromptRegex()
}

// ValidateBranchPattern compiles pattern as a worktree prompt-branch regex.
// An empty pattern is valid and returns (nil, nil). The returned compile
// error is bare; callers add field-name context when surfacing it.
func ValidateBranchPattern(pattern string) (*regexp.Regexp, error) {
if pattern == "" {
return nil, nil
}
return regexp.Compile(pattern)
}

// compilePromptRegex compiles and caches the worktree prompt-branch regex.
func (c *Config) compilePromptRegex() error {
re, err := ValidateBranchPattern(c.Worktree.PromptBranchPattern)
if err != nil {
return fmt.Errorf("invalid worktree.promptBranchPattern %q: %w", c.Worktree.PromptBranchPattern, err)
}
c.promptBranchRegex = re
return nil
}

// ShouldPromptForWorktree reports whether Chief should prompt the user about using a git worktree for the given branch.
func (c *Config) ShouldPromptForWorktree(branch string) bool {
if c.Worktree.AlwaysPrompt {
return true
}
if c.promptBranchRegex == nil {
return false
}
return c.promptBranchRegex.MatchString(branch)
}

// configPath returns the full path to the config file.
Expand Down Expand Up @@ -66,16 +119,23 @@ func Load(baseDir string) (*Config, error) {
if err := yaml.Unmarshal(data, cfg); err != nil {
return nil, err
}
if err := cfg.Validate(); err != nil {
return nil, err
}

return cfg, nil
}

// Save writes the config to .chief/config.yaml.
func Save(baseDir string, cfg *Config) error {
if err := cfg.Validate(); err != nil {
return err
}

path := configPath(baseDir)

// Ensure directory exists
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}

Expand All @@ -84,5 +144,5 @@ func Save(baseDir string, cfg *Config) error {
return err
}

return os.WriteFile(path, data, 0o644)
return os.WriteFile(path, data, 0644)
}
Loading