Skip to content

Commit b9cb4d3

Browse files
committed
Fix merge conflicts and format code
1 parent d8234cc commit b9cb4d3

19 files changed

Lines changed: 195 additions & 161 deletions

internal/cmd/convert.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@ import (
1313

1414
// runConversionWithProvider runs the agent to convert prd.md to JSON.
1515
func runConversionWithProvider(provider loop.Provider, absPRDDir string) (string, error) {
16-
content, err := os.ReadFile(filepath.Join(absPRDDir, "prd.md"))
17-
if err != nil {
18-
return "", fmt.Errorf("failed to read prd.md: %w", err)
19-
}
20-
prompt := embed.GetConvertPrompt(string(content))
16+
prompt := embed.GetConvertPrompt(filepath.Join(absPRDDir, "prd.md"), "US")
2117
cmd, mode, outPath, err := provider.ConvertCommand(absPRDDir, prompt)
2218
if err != nil {
2319
return "", fmt.Errorf("failed to prepare conversion command: %w", err)

internal/cmd/new.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func RunConvertWithOptions(opts ConvertOptions) error {
121121
PRDDir: opts.PRDDir,
122122
Merge: opts.Merge,
123123
Force: opts.Force,
124-
RunConversion: func(absPRDDir string) (string, error) {
124+
RunConversion: func(absPRDDir, idPrefix string) (string, error) {
125125
raw, err := runConversionWithProvider(provider, absPRDDir)
126126
if err != nil {
127127
return "", err

internal/config/config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ const configFile = ".chief/config.yaml"
1313
type Config struct {
1414
Worktree WorktreeConfig `yaml:"worktree"`
1515
OnComplete OnCompleteConfig `yaml:"onComplete"`
16-
Agent AgentConfig `yaml:"agent"`
16+
Agent AgentConfig `yaml:"agent"`
1717
}
1818

1919
// AgentConfig holds agent CLI settings (Claude, Codex, or OpenCode).
2020
type AgentConfig struct {
2121
Provider string `yaml:"provider"` // "claude" (default) | "codex" | "opencode"
22-
CLIPath string `yaml:"cliPath"` // optional custom path to CLI binary
22+
CLIPath string `yaml:"cliPath"` // optional custom path to CLI binary
2323
}
2424

2525
// WorktreeConfig holds worktree-related settings.

internal/loop/loop_test.go

Lines changed: 60 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"os"
8+
"os/exec"
89
"path/filepath"
910
"strings"
1011
"sync/atomic"
@@ -14,6 +15,44 @@ import (
1415
"github.com/minicodemonkey/chief/internal/prd"
1516
)
1617

18+
// mockProvider implements Provider for tests without importing agent (avoids import cycle).
19+
type mockProvider struct {
20+
cliPath string // if set, used as CLI path; otherwise "claude"
21+
}
22+
23+
func (m *mockProvider) Name() string { return "Test" }
24+
func (m *mockProvider) CLIPath() string { return m.path() }
25+
func (m *mockProvider) InteractiveCommand(_, _ string) *exec.Cmd { return exec.Command("true") }
26+
func (m *mockProvider) ParseLine(line string) *Event { return ParseLine(line) }
27+
func (m *mockProvider) LogFileName() string { return "claude.log" }
28+
29+
func (m *mockProvider) ConvertCommand(_, _ string) (*exec.Cmd, OutputMode, string, error) {
30+
return exec.Command("true"), OutputStdout, "", nil
31+
}
32+
33+
func (m *mockProvider) FixJSONCommand(_ string) (*exec.Cmd, OutputMode, string, error) {
34+
return exec.Command("true"), OutputStdout, "", nil
35+
}
36+
37+
func (m *mockProvider) path() string {
38+
if m.cliPath != "" {
39+
return m.cliPath
40+
}
41+
return "claude"
42+
}
43+
44+
func (m *mockProvider) LoopCommand(ctx context.Context, _, workDir string) *exec.Cmd {
45+
p := m.path()
46+
cmd := exec.CommandContext(ctx, p)
47+
cmd.Dir = workDir
48+
return cmd
49+
}
50+
51+
func (m *mockProvider) CleanOutput(output string) string { return output }
52+
53+
// testProvider is used by loop tests so they don't need to run a real CLI.
54+
var testProvider Provider = &mockProvider{}
55+
1756
// createMockClaudeScript creates a shell script that outputs predefined stream-json.
1857
func createMockClaudeScript(t *testing.T, dir string, output []string) string {
1958
t.Helper()
@@ -59,7 +98,7 @@ func createTestPRD(t *testing.T, dir string, allComplete bool) string {
5998
}
6099

61100
func TestNewLoop(t *testing.T) {
62-
l := NewLoop("/path/to/prd.json", "test prompt", 5)
101+
l := NewLoop("/path/to/prd.json", "test prompt", 5, testProvider)
63102

64103
if l.prdPath != "/path/to/prd.json" {
65104
t.Errorf("Expected prdPath %q, got %q", "/path/to/prd.json", l.prdPath)
@@ -76,7 +115,7 @@ func TestNewLoop(t *testing.T) {
76115
}
77116

78117
func TestNewLoopWithWorkDir(t *testing.T) {
79-
l := NewLoopWithWorkDir("/path/to/prd.json", "/work/dir", "test prompt", 5)
118+
l := NewLoopWithWorkDir("/path/to/prd.json", "/work/dir", "test prompt", 5, testProvider)
80119

81120
if l.prdPath != "/path/to/prd.json" {
82121
t.Errorf("Expected prdPath %q, got %q", "/path/to/prd.json", l.prdPath)
@@ -96,15 +135,15 @@ func TestNewLoopWithWorkDir(t *testing.T) {
96135
}
97136

98137
func TestNewLoopWithWorkDir_EmptyWorkDir(t *testing.T) {
99-
l := NewLoopWithWorkDir("/path/to/prd.json", "", "test prompt", 5)
138+
l := NewLoopWithWorkDir("/path/to/prd.json", "", "test prompt", 5, testProvider)
100139

101140
if l.workDir != "" {
102141
t.Errorf("Expected empty workDir, got %q", l.workDir)
103142
}
104143
}
105144

106145
func TestLoop_Events(t *testing.T) {
107-
l := NewLoop("/path/to/prd.json", "test prompt", 5)
146+
l := NewLoop("/path/to/prd.json", "test prompt", 5, testProvider)
108147
events := l.Events()
109148

110149
if events == nil {
@@ -113,7 +152,7 @@ func TestLoop_Events(t *testing.T) {
113152
}
114153

115154
func TestLoop_Iteration(t *testing.T) {
116-
l := NewLoop("/path/to/prd.json", "test prompt", 5)
155+
l := NewLoop("/path/to/prd.json", "test prompt", 5, testProvider)
117156

118157
if l.Iteration() != 0 {
119158
t.Errorf("Expected initial iteration to be 0, got %d", l.Iteration())
@@ -126,7 +165,7 @@ func TestLoop_Iteration(t *testing.T) {
126165
}
127166

128167
func TestLoop_Stop(t *testing.T) {
129-
l := NewLoop("/path/to/prd.json", "test prompt", 5)
168+
l := NewLoop("/path/to/prd.json", "test prompt", 5, testProvider)
130169

131170
l.Stop()
132171

@@ -162,7 +201,7 @@ func TestLoop_RunWithMockClaude(t *testing.T) {
162201

163202
// Create a prompt that invokes our mock script instead of real Claude
164203
// For the actual test, we'll test the internal methods
165-
l := NewLoop(prdPath, "test prompt", 1)
204+
l := NewLoop(prdPath, "test prompt", 1, testProvider)
166205

167206
// Override the command for testing - we'll test processOutput directly
168207
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
@@ -244,7 +283,7 @@ func TestLoop_MaxIterations(t *testing.T) {
244283
tmpDir := t.TempDir()
245284
prdPath := createTestPRD(t, tmpDir, false) // Not complete
246285

247-
l := NewLoop(prdPath, "test prompt", 2)
286+
l := NewLoop(prdPath, "test prompt", 2, testProvider)
248287

249288
// Simulate reaching max iterations by manually incrementing
250289
l.iteration = 2
@@ -290,7 +329,7 @@ func TestLoop_LogFile(t *testing.T) {
290329
t.Fatalf("Failed to create log file: %v", err)
291330
}
292331

293-
l := NewLoop(filepath.Join(tmpDir, "prd.json"), "test", 1)
332+
l := NewLoop(filepath.Join(tmpDir, "prd.json"), "test", 1, testProvider)
294333
l.logFile = logFile
295334

296335
l.logLine("test log line")
@@ -309,7 +348,7 @@ func TestLoop_LogFile(t *testing.T) {
309348

310349
// TestLoop_ChiefCompleteEvent tests detection of <chief-complete/> event.
311350
func TestLoop_ChiefCompleteEvent(t *testing.T) {
312-
l := NewLoop("/test/prd.json", "test", 5)
351+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
313352
l.iteration = 1
314353

315354
done := make(chan bool)
@@ -350,7 +389,7 @@ func TestLoop_ChiefCompleteEvent(t *testing.T) {
350389

351390
// TestLoop_SetMaxIterations tests setting max iterations at runtime.
352391
func TestLoop_SetMaxIterations(t *testing.T) {
353-
l := NewLoop("/test/prd.json", "test", 5)
392+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
354393

355394
if l.MaxIterations() != 5 {
356395
t.Errorf("Expected initial maxIter 5, got %d", l.MaxIterations())
@@ -380,7 +419,7 @@ func TestDefaultRetryConfig(t *testing.T) {
380419

381420
// TestLoop_SetRetryConfig tests setting retry config.
382421
func TestLoop_SetRetryConfig(t *testing.T) {
383-
l := NewLoop("/test/prd.json", "test", 5)
422+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
384423

385424
// Check default
386425
if !l.retryConfig.Enabled {
@@ -408,7 +447,7 @@ func TestLoop_SetRetryConfig(t *testing.T) {
408447

409448
// TestLoop_WatchdogDefaultTimeout tests that the default watchdog timeout is set.
410449
func TestLoop_WatchdogDefaultTimeout(t *testing.T) {
411-
l := NewLoop("/test/prd.json", "test", 5)
450+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
412451

413452
if l.WatchdogTimeout() != DefaultWatchdogTimeout {
414453
t.Errorf("Expected default watchdog timeout %v, got %v", DefaultWatchdogTimeout, l.WatchdogTimeout())
@@ -417,7 +456,7 @@ func TestLoop_WatchdogDefaultTimeout(t *testing.T) {
417456

418457
// TestLoop_SetWatchdogTimeout tests setting the watchdog timeout.
419458
func TestLoop_SetWatchdogTimeout(t *testing.T) {
420-
l := NewLoop("/test/prd.json", "test", 5)
459+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
421460

422461
l.SetWatchdogTimeout(10 * time.Minute)
423462
if l.WatchdogTimeout() != 10*time.Minute {
@@ -433,7 +472,7 @@ func TestLoop_SetWatchdogTimeout(t *testing.T) {
433472

434473
// TestLoop_WatchdogKillsHungProcess tests that a hung process is killed after timeout.
435474
func TestLoop_WatchdogKillsHungProcess(t *testing.T) {
436-
l := NewLoop("/test/prd.json", "test", 5)
475+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
437476
l.iteration = 1
438477

439478
// Use a very short timeout for testing
@@ -496,7 +535,7 @@ func TestLoop_WatchdogKillsHungProcess(t *testing.T) {
496535

497536
// TestLoop_WatchdogDoesNotFireForActiveProcess tests that an active process doesn't trigger the watchdog.
498537
func TestLoop_WatchdogDoesNotFireForActiveProcess(t *testing.T) {
499-
l := NewLoop("/test/prd.json", "test", 5)
538+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
500539
l.iteration = 1
501540

502541
// Use a timeout that's longer than our test
@@ -551,7 +590,7 @@ func TestLoop_WatchdogDoesNotFireForActiveProcess(t *testing.T) {
551590

552591
// TestLoop_WatchdogDisabledWithZeroTimeout tests that watchdog is disabled when timeout is 0.
553592
func TestLoop_WatchdogDisabledWithZeroTimeout(t *testing.T) {
554-
l := NewLoop("/test/prd.json", "test", 5)
593+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
555594
l.SetWatchdogTimeout(0)
556595

557596
if l.WatchdogTimeout() != 0 {
@@ -561,7 +600,7 @@ func TestLoop_WatchdogDisabledWithZeroTimeout(t *testing.T) {
561600
// Verify that runIteration would not start a watchdog
562601
// (tested indirectly: timeout == 0 means the if-block in runIteration is skipped)
563602
// We test this by verifying the constructor behavior and setter
564-
l2 := NewLoop("/test/prd.json", "test", 5)
603+
l2 := NewLoop("/test/prd.json", "test", 5, testProvider)
565604
l2.SetWatchdogTimeout(0)
566605

567606
l2.mu.Lock()
@@ -575,7 +614,7 @@ func TestLoop_WatchdogDisabledWithZeroTimeout(t *testing.T) {
575614

576615
// TestLoop_LastOutputTimeUpdated tests that lastOutputTime is updated on each scanner output.
577616
func TestLoop_LastOutputTimeUpdated(t *testing.T) {
578-
l := NewLoop("/test/prd.json", "test", 5)
617+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
579618
l.iteration = 1
580619

581620
// Drain events to avoid blocking
@@ -616,7 +655,7 @@ func TestLoop_LastOutputTimeUpdated(t *testing.T) {
616655
// that feeds into retry logic.
617656
func TestLoop_WatchdogReturnsError(t *testing.T) {
618657
// This test verifies the error message format that runIterationWithRetry will see
619-
l := NewLoop("/test/prd.json", "test", 5)
658+
l := NewLoop("/test/prd.json", "test", 5, testProvider)
620659
l.SetWatchdogTimeout(100 * time.Millisecond)
621660

622661
// The watchdog error message should contain "watchdog timeout"
@@ -630,7 +669,7 @@ func TestLoop_WatchdogReturnsError(t *testing.T) {
630669

631670
// TestLoop_WatchdogWithWorkDir tests that watchdog works with NewLoopWithWorkDir too.
632671
func TestLoop_WatchdogWithWorkDir(t *testing.T) {
633-
l := NewLoopWithWorkDir("/test/prd.json", "/work", "test", 5)
672+
l := NewLoopWithWorkDir("/test/prd.json", "/work", "test", 5, testProvider)
634673

635674
if l.WatchdogTimeout() != DefaultWatchdogTimeout {
636675
t.Errorf("Expected default watchdog timeout for NewLoopWithWorkDir, got %v", l.WatchdogTimeout())

internal/loop/opencode_parser.go

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,32 @@ import (
77
)
88

99
type opencodeEvent struct {
10-
Type string `json:"type"`
11-
Timestamp int64 `json:"timestamp"`
12-
SessionID string `json:"sessionID"`
10+
Type string `json:"type"`
11+
Timestamp int64 `json:"timestamp"`
12+
SessionID string `json:"sessionID"`
1313
Part *opencodePart `json:"part,omitempty"`
1414
Error *opencodeError `json:"error,omitempty"`
1515
}
1616

1717
type opencodePart struct {
18-
ID string `json:"id"`
19-
Type string `json:"type,omitempty"`
20-
Text string `json:"text,omitempty"`
21-
Tool string `json:"tool,omitempty"`
22-
CallID string `json:"callID,omitempty"`
23-
Reason string `json:"reason,omitempty"`
24-
Snapshot string `json:"snapshot,omitempty"`
25-
State *opencodeState `json:"state,omitempty"`
26-
Tokens *opencodeTokens `json:"tokens,omitempty"`
27-
Cost float64 `json:"cost,omitempty"`
18+
ID string `json:"id"`
19+
Type string `json:"type,omitempty"`
20+
Text string `json:"text,omitempty"`
21+
Tool string `json:"tool,omitempty"`
22+
CallID string `json:"callID,omitempty"`
23+
Reason string `json:"reason,omitempty"`
24+
Snapshot string `json:"snapshot,omitempty"`
25+
State *opencodeState `json:"state,omitempty"`
26+
Tokens *opencodeTokens `json:"tokens,omitempty"`
27+
Cost float64 `json:"cost,omitempty"`
2828
}
2929

3030
type opencodeState struct {
31-
Status string `json:"status"`
32-
Input map[string]interface{} `json:"input,omitempty"`
33-
Output string `json:"output,omitempty"`
34-
Title string `json:"title,omitempty"`
35-
Time *opencodeTime `json:"time,omitempty"`
31+
Status string `json:"status"`
32+
Input map[string]interface{} `json:"input,omitempty"`
33+
Output string `json:"output,omitempty"`
34+
Title string `json:"title,omitempty"`
35+
Time *opencodeTime `json:"time,omitempty"`
3636
}
3737

3838
type opencodeTime struct {
@@ -41,10 +41,10 @@ type opencodeTime struct {
4141
}
4242

4343
type opencodeTokens struct {
44-
Input int `json:"input"`
45-
Output int `json:"output"`
46-
Reasoning int `json:"reasoning"`
47-
Cache *opencodeCacheTokens `json:"cache,omitempty"`
44+
Input int `json:"input"`
45+
Output int `json:"output"`
46+
Reasoning int `json:"reasoning"`
47+
Cache *opencodeCacheTokens `json:"cache,omitempty"`
4848
}
4949

5050
type opencodeCacheTokens struct {
@@ -53,7 +53,7 @@ type opencodeCacheTokens struct {
5353
}
5454

5555
type opencodeError struct {
56-
Name string `json:"name"`
56+
Name string `json:"name"`
5757
Data *opencodeErrorData `json:"data,omitempty"`
5858
}
5959

internal/prd/generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ type ConvertOptions struct {
5858
Merge bool // Auto-merge progress on conversion conflicts
5959
Force bool // Auto-overwrite on conversion conflicts
6060
// RunConversion runs the agent to convert prd.md to JSON. Required.
61-
RunConversion func(absPRDDir string) (string, error)
61+
RunConversion func(absPRDDir, idPrefix string) (string, error)
6262
// RunFixJSON runs the agent to fix invalid JSON. Required.
6363
RunFixJSON func(prompt string) (string, error)
6464
}

internal/prd/generator_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -381,10 +381,10 @@ func TestMergeProgress(t *testing.T) {
381381
t.Run("mixed scenario - add, remove, keep", func(t *testing.T) {
382382
oldPRD := &PRD{
383383
UserStories: []UserStory{
384-
{ID: "US-001", Passes: true}, // Keep with progress
385-
{ID: "US-002", Passes: true}, // Removed
386-
{ID: "US-003", InProgress: true}, // Keep with progress
387-
{ID: "US-004", Passes: false}, // Keep without progress
384+
{ID: "US-001", Passes: true}, // Keep with progress
385+
{ID: "US-002", Passes: true}, // Removed
386+
{ID: "US-003", InProgress: true}, // Keep with progress
387+
{ID: "US-004", Passes: false}, // Keep without progress
388388
},
389389
}
390390
newPRD := &PRD{

internal/prd/watcher.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ type WatcherEvent struct {
1515

1616
// Watcher watches a prd.json file for changes and sends events.
1717
type Watcher struct {
18-
path string
19-
watcher *fsnotify.Watcher
20-
events chan WatcherEvent
21-
done chan struct{}
22-
mu sync.Mutex
23-
running bool
24-
lastPRD *PRD
18+
path string
19+
watcher *fsnotify.Watcher
20+
events chan WatcherEvent
21+
done chan struct{}
22+
mu sync.Mutex
23+
running bool
24+
lastPRD *PRD
2525
}
2626

2727
// NewWatcher creates a new Watcher for the given PRD file path.

0 commit comments

Comments
 (0)