Skip to content

Commit b80fd1e

Browse files
feat: Remove prd.json dependency
1 parent 36c98b9 commit b80fd1e

43 files changed

Lines changed: 1328 additions & 2626 deletions

Some content is hidden

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

cmd/chief/main.go

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ func findAvailablePRD() string {
9393

9494
for _, entry := range entries {
9595
if entry.IsDir() {
96-
prdPath := filepath.Join(prdsDir, entry.Name(), "prd.json")
96+
prdPath := filepath.Join(prdsDir, entry.Name(), "prd.md")
9797
if _, err := os.Stat(prdPath); err == nil {
9898
return prdPath
9999
}
@@ -113,7 +113,7 @@ func listAvailablePRDs() []string {
113113
var names []string
114114
for _, entry := range entries {
115115
if entry.IsDir() {
116-
prdPath := filepath.Join(prdsDir, entry.Name(), "prd.json")
116+
prdPath := filepath.Join(prdsDir, entry.Name(), "prd.md")
117117
if _, err := os.Stat(prdPath); err == nil {
118118
names = append(names, entry.Name())
119119
}
@@ -241,11 +241,11 @@ func parseTUIFlags() *TUIOptions {
241241
os.Exit(1)
242242
default:
243243
// Positional argument: PRD name or path
244-
if strings.HasSuffix(arg, ".json") || strings.HasSuffix(arg, "/") {
244+
if strings.HasSuffix(arg, ".md") || strings.HasSuffix(arg, ".json") || strings.HasSuffix(arg, "/") {
245245
opts.PRDPath = arg
246246
} else {
247247
// Treat as PRD name
248-
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.json", arg)
248+
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.md", arg)
249249
}
250250
}
251251
}
@@ -282,18 +282,11 @@ func runNew() {
282282
func runEdit() {
283283
opts := cmd.EditOptions{}
284284

285-
// Parse arguments: chief edit [name] [--merge] [--force] [--agent X] [--agent-path X]
285+
// Parse arguments: chief edit [name] [--agent X] [--agent-path X]
286286
flagAgent, flagPath, remaining := parseAgentFlags(os.Args, 2)
287287
for _, arg := range remaining {
288-
switch {
289-
case arg == "--merge":
290-
opts.Merge = true
291-
case arg == "--force":
292-
opts.Force = true
293-
default:
294-
if opts.Name == "" && !strings.HasPrefix(arg, "-") {
295-
opts.Name = arg
296-
}
288+
if opts.Name == "" && !strings.HasPrefix(arg, "-") {
289+
opts.Name = arg
297290
}
298291
}
299292

@@ -368,7 +361,7 @@ func runTUIWithOptions(opts *TUIOptions) {
368361
// If no PRD specified, try to find one
369362
if prdPath == "" {
370363
// Try "main" first
371-
mainPath := ".chief/prds/main/prd.json"
364+
mainPath := ".chief/prds/main/prd.md"
372365
if _, err := os.Stat(mainPath); err == nil {
373366
prdPath = mainPath
374367
} else {
@@ -411,30 +404,23 @@ func runTUIWithOptions(opts *TUIOptions) {
411404
}
412405

413406
// Restart TUI with the new PRD
414-
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.json", result.PRDName)
407+
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.md", result.PRDName)
415408
runTUIWithOptions(opts)
416409
return
417410
}
418411
}
419412

420413
prdDir := filepath.Dir(prdPath)
421414

422-
// Check if prd.md is newer than prd.json and run conversion if needed
423-
needsConvert, err := prd.NeedsConversion(prdDir)
424-
if err != nil {
425-
fmt.Printf("Warning: failed to check conversion status: %v\n", err)
426-
} else if needsConvert {
427-
fmt.Println("prd.md is newer than prd.json, running conversion...")
428-
if err := cmd.RunConvertWithOptions(cmd.ConvertOptions{
429-
PRDDir: prdDir,
430-
Merge: opts.Merge,
431-
Force: opts.Force,
432-
Provider: provider,
433-
}); err != nil {
434-
fmt.Printf("Error converting PRD: %v\n", err)
435-
os.Exit(1)
415+
// Auto-migrate: if prd.json exists alongside prd.md, migrate status
416+
jsonPath := filepath.Join(prdDir, "prd.json")
417+
if _, err := os.Stat(jsonPath); err == nil {
418+
fmt.Println("Migrating status from prd.json to prd.md...")
419+
if err := prd.MigrateFromJSON(prdDir); err != nil {
420+
fmt.Printf("Warning: migration failed: %v\n", err)
421+
} else {
422+
fmt.Println("Migration complete (prd.json renamed to prd.json.bak).")
436423
}
437-
fmt.Println("Conversion complete.")
438424
}
439425

440426
app, err := tui.NewAppWithOptions(prdPath, opts.MaxIterations, provider)
@@ -492,23 +478,21 @@ func runTUIWithOptions(opts *TUIOptions) {
492478
os.Exit(1)
493479
}
494480
// Restart TUI with the new PRD
495-
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.json", finalApp.PostExitPRD)
481+
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.md", finalApp.PostExitPRD)
496482
runTUIWithOptions(opts)
497483

498484
case tui.PostExitEdit:
499485
// Run edit command then restart TUI
500486
editOpts := cmd.EditOptions{
501487
Name: finalApp.PostExitPRD,
502-
Merge: opts.Merge,
503-
Force: opts.Force,
504488
Provider: provider,
505489
}
506490
if err := cmd.RunEdit(editOpts); err != nil {
507491
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
508492
os.Exit(1)
509493
}
510494
// Restart TUI with the edited PRD
511-
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.json", finalApp.PostExitPRD)
495+
opts.PRDPath = fmt.Sprintf(".chief/prds/%s/prd.md", finalApp.PostExitPRD)
512496
runTUIWithOptions(opts)
513497
}
514498
}
@@ -518,7 +502,7 @@ func printHelp() {
518502
fmt.Println(`Chief - Autonomous PRD Agent
519503
520504
Usage:
521-
chief [options] [<name>|<path/to/prd.json>]
505+
chief [options] [<name>|<path/to/prd.md>]
522506
chief <command> [arguments]
523507
524508
Commands:
@@ -545,13 +529,13 @@ Edit Options:
545529
--force Auto-overwrite on conversion conflicts
546530
547531
Positional Arguments:
548-
<name> PRD name (loads .chief/prds/<name>/prd.json)
549-
<path/to/prd.json> Direct path to a prd.json file
532+
<name> PRD name (loads .chief/prds/<name>/prd.md)
533+
<path/to/prd.md> Direct path to a prd.md file
550534
551535
Examples:
552536
chief Launch TUI with default PRD (.chief/prds/main/)
553537
chief auth Launch TUI with named PRD (.chief/prds/auth/)
554-
chief ./my-prd.json Launch TUI with specific PRD file
538+
chief ./my-prd.md Launch TUI with specific PRD file
555539
chief -n 20 Launch with 20 max iterations
556540
chief --max-iterations=5 auth
557541
Launch auth PRD with 5 max iterations

embed/convert_prompt.txt

Lines changed: 0 additions & 41 deletions
This file was deleted.

embed/embed.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,15 @@ var initPromptTemplate string
1616
//go:embed edit_prompt.txt
1717
var editPromptTemplate string
1818

19-
//go:embed convert_prompt.txt
20-
var convertPromptTemplate string
21-
2219
//go:embed detect_setup_prompt.txt
2320
var detectSetupPromptTemplate string
2421

25-
// GetPrompt returns the agent prompt with the PRD path, progress path, and
22+
// GetPrompt returns the agent prompt with the progress path and
2623
// current story context substituted. The storyContext is the JSON of the
2724
// current story to work on, inlined directly into the prompt so that the
28-
// agent does not need to read the entire prd.json file.
29-
func GetPrompt(prdPath, progressPath, storyContext, storyID, storyTitle string) string {
30-
result := strings.ReplaceAll(promptTemplate, "{{PRD_PATH}}", prdPath)
31-
result = strings.ReplaceAll(result, "{{PROGRESS_PATH}}", progressPath)
25+
// agent does not need to read the entire prd.md file.
26+
func GetPrompt(progressPath, storyContext, storyID, storyTitle string) string {
27+
result := strings.ReplaceAll(promptTemplate, "{{PROGRESS_PATH}}", progressPath)
3228
result = strings.ReplaceAll(result, "{{STORY_CONTEXT}}", storyContext)
3329
result = strings.ReplaceAll(result, "{{STORY_ID}}", storyID)
3430
return strings.ReplaceAll(result, "{{STORY_TITLE}}", storyTitle)
@@ -48,14 +44,6 @@ func GetEditPrompt(prdDir string) string {
4844
return strings.ReplaceAll(editPromptTemplate, "{{PRD_DIR}}", prdDir)
4945
}
5046

51-
// GetConvertPrompt returns the PRD converter prompt with the file path and ID prefix substituted.
52-
// Claude reads the file itself using file-reading tools instead of receiving inlined content.
53-
// The idPrefix determines the story ID convention (e.g., "US" → US-001, "MFR" → MFR-001).
54-
func GetConvertPrompt(prdFilePath, idPrefix string) string {
55-
result := strings.ReplaceAll(convertPromptTemplate, "{{PRD_FILE_PATH}}", prdFilePath)
56-
return strings.ReplaceAll(result, "{{ID_PREFIX}}", idPrefix)
57-
}
58-
5947
// GetDetectSetupPrompt returns the prompt for detecting project setup commands.
6048
func GetDetectSetupPrompt() string {
6149
return detectSetupPromptTemplate

embed/embed_test.go

Lines changed: 6 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,11 @@ import (
66
)
77

88
func TestGetPrompt(t *testing.T) {
9-
prdPath := "/path/to/prd.json"
109
progressPath := "/path/to/progress.md"
1110
storyContext := `{"id":"US-001","title":"Test Story"}`
12-
prompt := GetPrompt(prdPath, progressPath, storyContext, "US-001", "Test Story")
11+
prompt := GetPrompt(progressPath, storyContext, "US-001", "Test Story")
1312

1413
// Verify all placeholders were substituted
15-
if strings.Contains(prompt, "{{PRD_PATH}}") {
16-
t.Error("Expected {{PRD_PATH}} to be substituted")
17-
}
1814
if strings.Contains(prompt, "{{PROGRESS_PATH}}") {
1915
t.Error("Expected {{PROGRESS_PATH}} to be substituted")
2016
}
@@ -33,11 +29,6 @@ func TestGetPrompt(t *testing.T) {
3329
t.Error("Expected prompt to contain exact commit message 'feat: US-001 - Test Story'")
3430
}
3531

36-
// Verify the PRD path appears in the prompt
37-
if !strings.Contains(prompt, prdPath) {
38-
t.Errorf("Expected prompt to contain PRD path %q", prdPath)
39-
}
40-
4132
// Verify the progress path appears in the prompt
4233
if !strings.Contains(prompt, progressPath) {
4334
t.Errorf("Expected prompt to contain progress path %q", progressPath)
@@ -48,22 +39,14 @@ func TestGetPrompt(t *testing.T) {
4839
t.Error("Expected prompt to contain inlined story context")
4940
}
5041

51-
// Verify the prompt contains key instructions
52-
if !strings.Contains(prompt, "chief-complete") {
53-
t.Error("Expected prompt to contain chief-complete instruction")
54-
}
55-
56-
if !strings.Contains(prompt, "ralph-status") {
57-
t.Error("Expected prompt to contain ralph-status instruction")
58-
}
59-
60-
if !strings.Contains(prompt, "passes: true") {
61-
t.Error("Expected prompt to contain passes: true instruction")
42+
// Verify the prompt contains chief-done stop condition
43+
if !strings.Contains(prompt, "chief-done") {
44+
t.Error("Expected prompt to contain chief-done instruction")
6245
}
6346
}
6447

6548
func TestGetPrompt_NoFileReadInstruction(t *testing.T) {
66-
prompt := GetPrompt("/path/prd.json", "/path/progress.md", `{"id":"US-001"}`, "US-001", "Test Story")
49+
prompt := GetPrompt("/path/progress.md", `{"id":"US-001"}`, "US-001", "Test Story")
6750

6851
// The prompt should NOT instruct Claude to read the PRD file
6952
if strings.Contains(prompt, "Read the PRD") {
@@ -78,7 +61,7 @@ func TestPromptTemplateNotEmpty(t *testing.T) {
7861
}
7962

8063
func TestGetPrompt_ChiefExclusion(t *testing.T) {
81-
prompt := GetPrompt("/path/prd.json", "/path/progress.md", `{"id":"US-001"}`, "US-001", "Test Story")
64+
prompt := GetPrompt("/path/progress.md", `{"id":"US-001"}`, "US-001", "Test Story")
8265

8366
// The prompt must instruct Claude to never stage or commit .chief/ files
8467
if !strings.Contains(prompt, ".chief/") {
@@ -87,74 +70,6 @@ func TestGetPrompt_ChiefExclusion(t *testing.T) {
8770
if !strings.Contains(prompt, "NEVER stage or commit") {
8871
t.Error("Expected prompt to explicitly say NEVER stage or commit .chief/ files")
8972
}
90-
// The commit step should not say "commit ALL changes" anymore
91-
if strings.Contains(prompt, "commit ALL changes") {
92-
t.Error("Expected prompt to NOT say 'commit ALL changes' — it should exclude .chief/ files")
93-
}
94-
}
95-
96-
func TestGetConvertPrompt(t *testing.T) {
97-
prdFilePath := "/path/to/prds/main/prd.md"
98-
prompt := GetConvertPrompt(prdFilePath, "US")
99-
100-
// Verify the prompt is not empty
101-
if prompt == "" {
102-
t.Error("Expected GetConvertPrompt() to return non-empty prompt")
103-
}
104-
105-
// Verify file path is substituted (not inlined content)
106-
if !strings.Contains(prompt, prdFilePath) {
107-
t.Error("Expected prompt to contain the PRD file path")
108-
}
109-
if strings.Contains(prompt, "{{PRD_FILE_PATH}}") {
110-
t.Error("Expected {{PRD_FILE_PATH}} to be substituted")
111-
}
112-
113-
// Verify the old {{PRD_CONTENT}} placeholder is completely removed
114-
if strings.Contains(prompt, "{{PRD_CONTENT}}") {
115-
t.Error("Expected {{PRD_CONTENT}} placeholder to be completely removed")
116-
}
117-
118-
// Verify ID prefix is substituted
119-
if strings.Contains(prompt, "{{ID_PREFIX}}") {
120-
t.Error("Expected {{ID_PREFIX}} to be substituted")
121-
}
122-
if !strings.Contains(prompt, "US-001") {
123-
t.Error("Expected prompt to contain US-001 when prefix is US")
124-
}
125-
126-
// Verify key instructions are present
127-
if !strings.Contains(prompt, "JSON") {
128-
t.Error("Expected prompt to mention JSON")
129-
}
130-
131-
if !strings.Contains(prompt, "userStories") {
132-
t.Error("Expected prompt to describe userStories structure")
133-
}
134-
135-
if !strings.Contains(prompt, `"passes": false`) {
136-
t.Error("Expected prompt to specify passes: false default")
137-
}
138-
139-
// Verify prompt instructs Claude to read the file
140-
if !strings.Contains(prompt, "Read the PRD file") {
141-
t.Error("Expected prompt to instruct Claude to read the PRD file")
142-
}
143-
}
144-
145-
func TestGetConvertPrompt_CustomPrefix(t *testing.T) {
146-
prompt := GetConvertPrompt("/path/prd.md", "MFR")
147-
148-
// Verify custom prefix is used, not hardcoded US
149-
if strings.Contains(prompt, "{{ID_PREFIX}}") {
150-
t.Error("Expected {{ID_PREFIX}} to be substituted")
151-
}
152-
if !strings.Contains(prompt, "MFR-001") {
153-
t.Error("Expected prompt to contain MFR-001 when prefix is MFR")
154-
}
155-
if !strings.Contains(prompt, "MFR-002") {
156-
t.Error("Expected prompt to contain MFR-002 when prefix is MFR")
157-
}
15873
}
15974

16075
func TestGetInitPrompt(t *testing.T) {

0 commit comments

Comments
 (0)