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
8 changes: 5 additions & 3 deletions cmd/chief/commands/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,11 @@ func runTUI(command *cobra.Command, args []string) error {
func runTUIWithOptions(prdPath string, provider loop.Provider) error {
// If no PRD specified, try to find one
if prdPath == "" {
mainPath := ".chief/prds/main/prd.md"
if _, err := os.Stat(mainPath); err == nil {
prdPath = mainPath
// Try "default" first, falling back to the legacy "main" name so
// existing projects created before the rename keep working.
defaultPath := fmt.Sprintf(".chief/prds/%s/prd.md", cmd.ResolveDefaultPRDName("."))
if _, err := os.Stat(defaultPath); err == nil {
prdPath = defaultPath
} else {
prdPath = findAvailablePRD()
}
Expand Down
4 changes: 2 additions & 2 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ chief new [name] [context]

| Argument | Description |
|----------|-------------|
| `name` | PRD name (optional, defaults to `main`). Must contain only letters, numbers, hyphens, and underscores. |
| `name` | PRD name (optional, defaults to `default`). Must contain only letters, numbers, hyphens, and underscores. |
| `context` | Additional context to pass to the agent (optional). Included in the PRD creation prompt. |

**How it works:**
Expand All @@ -108,7 +108,7 @@ chief new [name] [context]
**Examples:**

```bash
# Create a new PRD (defaults to name "main")
# Create a new PRD (defaults to name "default")
chief new

# Create a named PRD
Expand Down
8 changes: 4 additions & 4 deletions internal/cmd/edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ import (

// EditOptions contains configuration for the edit command.
type EditOptions struct {
Name string // PRD name (default: "main")
Name string // PRD name (default: "default")
BaseDir string // Base directory for .chief/prds/ (default: current directory)
Provider loop.Provider // Agent CLI provider (Claude or Codex)
}

// RunEdit edits an existing PRD by launching an interactive Claude session.
func RunEdit(opts EditOptions) error {
// Set defaults
if opts.Name == "" {
opts.Name = "main"
}
if opts.BaseDir == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
opts.BaseDir = cwd
}
if opts.Name == "" {
opts.Name = ResolveDefaultPRDName(opts.BaseDir)
}

// Validate name
if !isValidPRDName(opts.Name) {
Expand Down
30 changes: 17 additions & 13 deletions internal/cmd/edit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ func TestRunEditRejectsInvalidName(t *testing.T) {
}
}

func TestRunEditDefaultsToMain(t *testing.T) {
func TestRunEditDefaultsToLegacyMainForBackwardCompat(t *testing.T) {
tmpDir := t.TempDir()

// Create main prd.md
// Create main prd.md (the pre-rename default location)
prdDir := filepath.Join(tmpDir, ".chief", "prds", "main")
if err := os.MkdirAll(prdDir, 0755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
Expand All @@ -55,21 +55,25 @@ func TestRunEditDefaultsToMain(t *testing.T) {
t.Fatalf("Failed to create prd.md: %v", err)
}

// Test with empty name (should default to main)
opts := EditOptions{
Name: "", // Empty should default to "main"
BaseDir: tmpDir,
// Existing projects created before the "default" rename must keep
// resolving to their "main" PRD when no name is given.
resolved := ResolveDefaultPRDName(tmpDir)
if resolved != "main" {
t.Errorf("Expected empty name to resolve to legacy 'main', got %q", resolved)
}

// We can't fully test RunEdit without Claude, but we can verify
// the name defaulting logic by checking if it would find the file
if opts.Name == "" {
opts.Name = "main"
prdPath := filepath.Join(tmpDir, ".chief", "prds", resolved, "prd.md")
if _, err := os.Stat(prdPath); os.IsNotExist(err) {
t.Error("Expected default name to resolve to existing prd.md")
}
}

prdPath := filepath.Join(tmpDir, ".chief", "prds", opts.Name, "prd.md")
if _, err := os.Stat(prdPath); os.IsNotExist(err) {
t.Error("Expected default name 'main' to resolve to existing prd.md")
func TestRunEditDefaultsToDefaultForNewProjects(t *testing.T) {
tmpDir := t.TempDir()

resolved := ResolveDefaultPRDName(tmpDir)
if resolved != "default" {
t.Errorf("Expected empty name to resolve to 'default' when no PRD exists, got %q", resolved)
}
}

Expand Down
34 changes: 30 additions & 4 deletions internal/cmd/new.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (

// NewOptions contains configuration for the new command.
type NewOptions struct {
Name string // PRD name (default: "main")
Name string // PRD name (default: "default")
Context string // Optional context to pass to the agent
BaseDir string // Base directory for .chief/prds/ (default: current directory)
Provider loop.Provider // Agent CLI provider (Claude or Codex)
Expand All @@ -24,16 +24,16 @@ type NewOptions struct {
// RunNew creates a new PRD by launching an interactive agent session.
func RunNew(opts NewOptions) error {
// Set defaults
if opts.Name == "" {
opts.Name = "main"
}
if opts.BaseDir == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
opts.BaseDir = cwd
}
if opts.Name == "" {
opts.Name = ResolveDefaultPRDName(opts.BaseDir)
}

// Validate name (alphanumeric, -, _)
if !isValidPRDName(opts.Name) {
Expand Down Expand Up @@ -111,3 +111,29 @@ func isValidPRDName(name string) bool {
}
return true
}

// DefaultPRDName is the name used for the default PRD directory
// (.chief/prds/default/) when no name is given.
const DefaultPRDName = "default"

// legacyDefaultPRDName is the default PRD directory name used before the
// rename to "default" and is kept for backward compatibility.
const legacyDefaultPRDName = "main"

// ResolveDefaultPRDName determines which PRD name to use when the caller
// didn't specify one. It prefers "default", the current convention, but
// falls back to "main" when a PRD already exists there and no "default"
// PRD has been created yet, so existing projects keep working unchanged.
func ResolveDefaultPRDName(baseDir string) string {
defaultPath := filepath.Join(baseDir, ".chief", "prds", DefaultPRDName, "prd.md")
if _, err := os.Stat(defaultPath); err == nil {
return DefaultPRDName
}

legacyPath := filepath.Join(baseDir, ".chief", "prds", legacyDefaultPRDName, "prd.md")
if _, err := os.Stat(legacyPath); err == nil {
return legacyDefaultPRDName
}

return DefaultPRDName
}
19 changes: 19 additions & 0 deletions internal/cmd/new_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,25 @@ func TestIsValidPRDName(t *testing.T) {
}
}

func TestRunNewDefaultNameTargetsExistingLegacyMain(t *testing.T) {
tmpDir := t.TempDir()

// A project created before the "default" rename has its PRD at "main".
prdDir := filepath.Join(tmpDir, ".chief", "prds", "main")
if err := os.MkdirAll(prdDir, 0755); err != nil {
t.Fatalf("Failed to create directory: %v", err)
}
if err := os.WriteFile(filepath.Join(prdDir, "prd.md"), []byte("# Main PRD"), 0644); err != nil {
t.Fatalf("Failed to create prd.md: %v", err)
}

opts := NewOptions{BaseDir: tmpDir}
err := RunNew(opts)
if err == nil || !strings.Contains(err.Error(), "already exists") {
t.Errorf("Expected 'chief new' with no name to resolve to the existing 'main' PRD and report it already exists, got: %v", err)
}
}

func TestRunNewCreatesDirectory(t *testing.T) {
// Create a temporary directory for testing
tmpDir := t.TempDir()
Expand Down
8 changes: 4 additions & 4 deletions internal/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,24 @@ import (

// StatusOptions contains configuration for the status command.
type StatusOptions struct {
Name string // PRD name (default: "main")
Name string // PRD name (default: "default")
BaseDir string // Base directory for .chief/prds/ (default: current directory)
}

// RunStatus prints progress for a PRD.
// Returns nil on success, error otherwise. Exit code should be 0 on success.
func RunStatus(opts StatusOptions) error {
// Set defaults
if opts.Name == "" {
opts.Name = "main"
}
if opts.BaseDir == "" {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("failed to get current directory: %w", err)
}
opts.BaseDir = cwd
}
if opts.Name == "" {
opts.Name = ResolveDefaultPRDName(opts.BaseDir)
}

// Build PRD path
prdPath := filepath.Join(opts.BaseDir, ".chief", "prds", opts.Name, "prd.md")
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/first_time_setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func NewFirstTimeSetup(baseDir string, showGitignore bool) *FirstTimeSetup {
ti.Placeholder = ""
ti.CharLimit = maxPRDNameLength
ti.Width = prdNameModalWidth(0) - 8
ti.SetValue("main")
ti.SetValue("default")
ti.CursorEnd()
ti.Focus()

Expand Down
4 changes: 2 additions & 2 deletions internal/tui/first_time_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ func sendKey(t *testing.T, f FirstTimeSetup, msg tea.KeyMsg) FirstTimeSetup {

func TestPRDName_InitialCursorAtEnd(t *testing.T) {
setup := NewFirstTimeSetup(t.TempDir(), false)
if got, want := setup.ti.Value(), "main"; got != want {
if got, want := setup.ti.Value(), "default"; got != want {
t.Fatalf("initial value: got %q, want %q", got, want)
}
if got, want := setup.ti.Position(), len("main"); got != want {
if got, want := setup.ti.Position(), len("default"); got != want {
t.Fatalf("initial cursor position: got %d, want %d", got, want)
}
}
Expand Down