Skip to content

Commit 8313c65

Browse files
committed
Add errors to Provider convert/fix and resolver
Propagate errors from provider commands and improve provider resolution flow. - Extended loop.Provider ConvertCommand and FixJSONCommand to return an error and updated implementations (Claude, Codex) to follow the new signature. Codex now returns explicit errors when temp files cannot be created. - Updated callers (internal/cmd/convert.go, runFixJSONWithProvider, loop tests, agent tests) to handle the new error return values. - Changed Resolve to return (loop.Provider, error) and make unknown providers produce a clear error; added mustResolve helper in tests and a TestResolve_unknownProvider. - Added resolveProvider helper in cmd/chief to load config and resolve the provider (exiting on error), and improved CLI flag parsing for --agent/--agent-path (support both --flag value and --flag=value). Minor test and docstring tweaks to reflect agent-agnostic language.
1 parent 96fd2f6 commit 8313c65

10 files changed

Lines changed: 129 additions & 79 deletions

File tree

cmd/chief/main.go

Lines changed: 51 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/minicodemonkey/chief/internal/cmd"
1313
"github.com/minicodemonkey/chief/internal/config"
1414
"github.com/minicodemonkey/chief/internal/git"
15+
"github.com/minicodemonkey/chief/internal/loop"
1516
"github.com/minicodemonkey/chief/internal/prd"
1617
"github.com/minicodemonkey/chief/internal/tui"
1718
)
@@ -227,37 +228,41 @@ func parseTUIFlags() *TUIOptions {
227228

228229
func runNew() {
229230
opts := cmd.NewOptions{}
230-
// Parse arguments: chief new [name] [context...]
231-
if len(os.Args) > 2 {
232-
opts.Name = os.Args[2]
233-
}
234-
if len(os.Args) > 3 {
235-
opts.Context = strings.Join(os.Args[3:], " ")
236-
}
237-
// Resolve provider (support --agent/--agent-path after "new")
238-
cwd, _ := os.Getwd()
239-
cfg, _ := config.Load(cwd)
240231
flagAgent, flagPath := "", ""
232+
var positional []string
233+
234+
// Parse arguments: chief new [name] [context...] [--agent X] [--agent-path X]
241235
for i := 2; i < len(os.Args); i++ {
242-
switch os.Args[i] {
243-
case "--agent":
236+
arg := os.Args[i]
237+
switch {
238+
case arg == "--agent":
244239
if i+1 < len(os.Args) {
245240
i++
246241
flagAgent = os.Args[i]
247242
}
248-
case "--agent-path":
243+
case strings.HasPrefix(arg, "--agent="):
244+
flagAgent = strings.TrimPrefix(arg, "--agent=")
245+
case arg == "--agent-path":
249246
if i+1 < len(os.Args) {
250247
i++
251248
flagPath = os.Args[i]
252249
}
250+
case strings.HasPrefix(arg, "--agent-path="):
251+
flagPath = strings.TrimPrefix(arg, "--agent-path=")
252+
case strings.HasPrefix(arg, "-"):
253+
// skip unknown flags
254+
default:
255+
positional = append(positional, arg)
253256
}
254257
}
255-
opts.Provider = agent.Resolve(flagAgent, flagPath, cfg)
256-
if err := agent.CheckInstalled(opts.Provider); err != nil {
257-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
258-
os.Exit(1)
258+
if len(positional) > 0 {
259+
opts.Name = positional[0]
260+
}
261+
if len(positional) > 1 {
262+
opts.Context = strings.Join(positional[1:], " ")
259263
}
260264

265+
opts.Provider = resolveProvider(flagAgent, flagPath)
261266
if err := cmd.RunNew(opts); err != nil {
262267
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
263268
os.Exit(1)
@@ -267,38 +272,37 @@ func runNew() {
267272
func runEdit() {
268273
opts := cmd.EditOptions{}
269274
flagAgent, flagPath := "", ""
270-
// Parse arguments: chief edit [name] [--merge] [--force] [--agent] [--agent-path]
275+
276+
// Parse arguments: chief edit [name] [--merge] [--force] [--agent X] [--agent-path X]
271277
for i := 2; i < len(os.Args); i++ {
272278
arg := os.Args[i]
273-
switch arg {
274-
case "--merge":
279+
switch {
280+
case arg == "--merge":
275281
opts.Merge = true
276-
case "--force":
282+
case arg == "--force":
277283
opts.Force = true
278-
case "--agent":
284+
case arg == "--agent":
279285
if i+1 < len(os.Args) {
280286
i++
281287
flagAgent = os.Args[i]
282288
}
283-
case "--agent-path":
289+
case strings.HasPrefix(arg, "--agent="):
290+
flagAgent = strings.TrimPrefix(arg, "--agent=")
291+
case arg == "--agent-path":
284292
if i+1 < len(os.Args) {
285293
i++
286294
flagPath = os.Args[i]
287295
}
296+
case strings.HasPrefix(arg, "--agent-path="):
297+
flagPath = strings.TrimPrefix(arg, "--agent-path=")
288298
default:
289299
if opts.Name == "" && !strings.HasPrefix(arg, "-") {
290300
opts.Name = arg
291301
}
292302
}
293303
}
294-
cwd, _ := os.Getwd()
295-
cfg, _ := config.Load(cwd)
296-
opts.Provider = agent.Resolve(flagAgent, flagPath, cfg)
297-
if err := agent.CheckInstalled(opts.Provider); err != nil {
298-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
299-
os.Exit(1)
300-
}
301304

305+
opts.Provider = resolveProvider(flagAgent, flagPath)
302306
if err := cmd.RunEdit(opts); err != nil {
303307
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
304308
os.Exit(1)
@@ -337,15 +341,28 @@ func runList() {
337341
}
338342
}
339343

340-
func runTUIWithOptions(opts *TUIOptions) {
341-
// Resolve agent provider early (used for conversion, app, new, edit)
342-
cwd, _ := os.Getwd()
344+
// resolveProvider loads config and resolves the agent provider, exiting on error.
345+
func resolveProvider(flagAgent, flagPath string) loop.Provider {
346+
cwd, err := os.Getwd()
347+
if err != nil {
348+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
349+
os.Exit(1)
350+
}
343351
cfg, _ := config.Load(cwd)
344-
provider := agent.Resolve(opts.Agent, opts.AgentPath, cfg)
352+
provider, err := agent.Resolve(flagAgent, flagPath, cfg)
353+
if err != nil {
354+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
355+
os.Exit(1)
356+
}
345357
if err := agent.CheckInstalled(provider); err != nil {
346358
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
347359
os.Exit(1)
348360
}
361+
return provider
362+
}
363+
364+
func runTUIWithOptions(opts *TUIOptions) {
365+
provider := resolveProvider(opts.Agent, opts.AgentPath)
349366

350367
prdPath := opts.PRDPath
351368

internal/agent/claude.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,17 @@ func (p *ClaudeProvider) InteractiveCommand(workDir, prompt string) *exec.Cmd {
4848
}
4949

5050
// ConvertCommand implements loop.Provider.
51-
func (p *ClaudeProvider) ConvertCommand(workDir, prompt string) (*exec.Cmd, loop.OutputMode, string) {
51+
func (p *ClaudeProvider) ConvertCommand(workDir, prompt string) (*exec.Cmd, loop.OutputMode, string, error) {
5252
cmd := exec.Command(p.cliPath, "-p", "--tools", "")
5353
cmd.Dir = workDir
5454
cmd.Stdin = strings.NewReader(prompt)
55-
return cmd, loop.OutputStdout, ""
55+
return cmd, loop.OutputStdout, "", nil
5656
}
5757

5858
// FixJSONCommand implements loop.Provider.
59-
func (p *ClaudeProvider) FixJSONCommand(prompt string) (*exec.Cmd, loop.OutputMode, string) {
59+
func (p *ClaudeProvider) FixJSONCommand(prompt string) (*exec.Cmd, loop.OutputMode, string, error) {
6060
cmd := exec.Command(p.cliPath, "-p", prompt)
61-
return cmd, loop.OutputStdout, ""
61+
return cmd, loop.OutputStdout, "", nil
6262
}
6363

6464
// ParseLine implements loop.Provider.

internal/agent/codex.go

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package agent
22

33
import (
44
"context"
5+
"fmt"
56
"os"
67
"os/exec"
78
"strings"
@@ -45,36 +46,30 @@ func (p *CodexProvider) InteractiveCommand(workDir, prompt string) *exec.Cmd {
4546
}
4647

4748
// ConvertCommand implements loop.Provider.
48-
func (p *CodexProvider) ConvertCommand(workDir, prompt string) (*exec.Cmd, loop.OutputMode, string) {
49+
func (p *CodexProvider) ConvertCommand(workDir, prompt string) (*exec.Cmd, loop.OutputMode, string, error) {
4950
f, err := os.CreateTemp("", "chief-codex-convert-*.txt")
5051
if err != nil {
51-
// Caller will fail when running cmd; return empty path
52-
cmd := exec.Command(p.cliPath, "exec", "--sandbox", "read-only", "--output-last-message", "-o", "", "-")
53-
cmd.Dir = workDir
54-
cmd.Stdin = strings.NewReader(prompt)
55-
return cmd, loop.OutputFromFile, ""
52+
return nil, 0, "", fmt.Errorf("failed to create temp file for conversion output: %w", err)
5653
}
5754
outPath := f.Name()
5855
f.Close()
5956
cmd := exec.Command(p.cliPath, "exec", "--sandbox", "read-only", "--output-last-message", "-o", outPath, "-")
6057
cmd.Dir = workDir
6158
cmd.Stdin = strings.NewReader(prompt)
62-
return cmd, loop.OutputFromFile, outPath
59+
return cmd, loop.OutputFromFile, outPath, nil
6360
}
6461

6562
// FixJSONCommand implements loop.Provider.
66-
func (p *CodexProvider) FixJSONCommand(prompt string) (*exec.Cmd, loop.OutputMode, string) {
63+
func (p *CodexProvider) FixJSONCommand(prompt string) (*exec.Cmd, loop.OutputMode, string, error) {
6764
f, err := os.CreateTemp("", "chief-codex-fixjson-*.txt")
6865
if err != nil {
69-
cmd := exec.Command(p.cliPath, "exec", "--sandbox", "read-only", "--output-last-message", "-o", "", "-")
70-
cmd.Stdin = strings.NewReader(prompt)
71-
return cmd, loop.OutputFromFile, ""
66+
return nil, 0, "", fmt.Errorf("failed to create temp file for fix output: %w", err)
7267
}
7368
outPath := f.Name()
7469
f.Close()
7570
cmd := exec.Command(p.cliPath, "exec", "--sandbox", "read-only", "--output-last-message", "-o", outPath, "-")
7671
cmd.Stdin = strings.NewReader(prompt)
77-
return cmd, loop.OutputFromFile, outPath
72+
return cmd, loop.OutputFromFile, outPath, nil
7873
}
7974

8075
// ParseLine implements loop.Provider.

internal/agent/codex_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,10 @@ func TestCodexProvider_LoopCommand(t *testing.T) {
6262

6363
func TestCodexProvider_ConvertCommand(t *testing.T) {
6464
p := NewCodexProvider("codex")
65-
cmd, mode, outPath := p.ConvertCommand("/prd/dir", "convert prompt")
65+
cmd, mode, outPath, err := p.ConvertCommand("/prd/dir", "convert prompt")
66+
if err != nil {
67+
t.Fatalf("ConvertCommand unexpected error: %v", err)
68+
}
6669
if mode != loop.OutputFromFile {
6770
t.Errorf("ConvertCommand mode = %v, want OutputFromFile", mode)
6871
}
@@ -72,7 +75,6 @@ func TestCodexProvider_ConvertCommand(t *testing.T) {
7275
if !strings.Contains(cmd.Path, "codex") {
7376
t.Errorf("ConvertCommand Path = %q", cmd.Path)
7477
}
75-
// Should have -o outPath
7678
foundO := false
7779
for i, a := range cmd.Args {
7880
if a == "-o" && i+1 < len(cmd.Args) && cmd.Args[i+1] == outPath {
@@ -90,14 +92,16 @@ func TestCodexProvider_ConvertCommand(t *testing.T) {
9092

9193
func TestCodexProvider_FixJSONCommand(t *testing.T) {
9294
p := NewCodexProvider("codex")
93-
cmd, mode, outPath := p.FixJSONCommand("fix prompt")
95+
cmd, mode, outPath, err := p.FixJSONCommand("fix prompt")
96+
if err != nil {
97+
t.Fatalf("FixJSONCommand unexpected error: %v", err)
98+
}
9499
if mode != loop.OutputFromFile {
95100
t.Errorf("FixJSONCommand mode = %v, want OutputFromFile", mode)
96101
}
97102
if outPath == "" {
98103
t.Error("FixJSONCommand outPath should be non-empty temp file")
99104
}
100-
// -o outPath present
101105
foundO := false
102106
for i, a := range cmd.Args {
103107
if a == "-o" && i+1 < len(cmd.Args) && cmd.Args[i+1] == outPath {

internal/agent/resolve.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import (
1212

1313
// Resolve returns the agent Provider using priority: flagAgent > CHIEF_AGENT env > config > "claude".
1414
// flagPath overrides the CLI path when non-empty (flag > CHIEF_AGENT_PATH > config agent.cliPath).
15-
func Resolve(flagAgent, flagPath string, cfg *config.Config) loop.Provider {
15+
// Returns an error if the resolved provider name is not recognised.
16+
func Resolve(flagAgent, flagPath string, cfg *config.Config) (loop.Provider, error) {
1617
providerName := "claude"
1718
if flagAgent != "" {
1819
providerName = strings.ToLower(strings.TrimSpace(flagAgent))
@@ -32,10 +33,12 @@ func Resolve(flagAgent, flagPath string, cfg *config.Config) loop.Provider {
3233
}
3334

3435
switch providerName {
36+
case "claude":
37+
return NewClaudeProvider(cliPath), nil
3538
case "codex":
36-
return NewCodexProvider(cliPath)
39+
return NewCodexProvider(cliPath), nil
3740
default:
38-
return NewClaudeProvider(cliPath)
41+
return nil, fmt.Errorf("unknown agent provider %q: expected \"claude\" or \"codex\"", providerName)
3942
}
4043
}
4144

internal/agent/resolve_test.go

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@ import (
88
"testing"
99

1010
"github.com/minicodemonkey/chief/internal/config"
11+
"github.com/minicodemonkey/chief/internal/loop"
1112
)
1213

14+
func mustResolve(t *testing.T, flagAgent, flagPath string, cfg *config.Config) loop.Provider {
15+
t.Helper()
16+
p, err := Resolve(flagAgent, flagPath, cfg)
17+
if err != nil {
18+
t.Fatalf("Resolve(%q, %q, cfg) unexpected error: %v", flagAgent, flagPath, err)
19+
}
20+
return p
21+
}
22+
1323
func TestResolve_priority(t *testing.T) {
1424
// Default: no flag, no env, nil config -> Claude
15-
got := Resolve("", "", nil)
25+
got := mustResolve(t, "", "", nil)
1626
if got.Name() != "Claude" {
1727
t.Errorf("Resolve(_, _, nil) name = %q, want Claude", got.Name())
1828
}
@@ -21,7 +31,7 @@ func TestResolve_priority(t *testing.T) {
2131
}
2232

2333
// Flag overrides everything
24-
got = Resolve("codex", "", nil)
34+
got = mustResolve(t, "codex", "", nil)
2535
if got.Name() != "Codex" {
2636
t.Errorf("Resolve(codex, _, nil) name = %q, want Codex", got.Name())
2737
}
@@ -30,7 +40,7 @@ func TestResolve_priority(t *testing.T) {
3040
cfg := &config.Config{}
3141
cfg.Agent.Provider = "codex"
3242
cfg.Agent.CLIPath = "/usr/local/bin/codex"
33-
got = Resolve("", "", cfg)
43+
got = mustResolve(t, "", "", cfg)
3444
if got.Name() != "Codex" {
3545
t.Errorf("Resolve(_, _, config codex) name = %q, want Codex", got.Name())
3646
}
@@ -39,12 +49,12 @@ func TestResolve_priority(t *testing.T) {
3949
}
4050

4151
// Flag overrides config
42-
got = Resolve("claude", "", cfg)
52+
got = mustResolve(t, "claude", "", cfg)
4353
if got.Name() != "Claude" {
4454
t.Errorf("Resolve(claude, _, config codex) name = %q, want Claude", got.Name())
4555
}
4656
// flag path overrides config path
47-
got = Resolve("codex", "/opt/codex", cfg)
57+
got = mustResolve(t, "codex", "/opt/codex", cfg)
4858
if got.CLIPath() != "/opt/codex" {
4959
t.Errorf("Resolve(codex, /opt/codex, cfg) CLIPath = %q, want /opt/codex", got.CLIPath())
5060
}
@@ -73,7 +83,7 @@ func TestResolve_env(t *testing.T) {
7383

7484
// Env provider when no flag
7585
os.Setenv(keyAgent, "codex")
76-
got := Resolve("", "", nil)
86+
got := mustResolve(t, "", "", nil)
7787
if got.Name() != "Codex" {
7888
t.Errorf("with CHIEF_AGENT=codex, name = %q, want Codex", got.Name())
7989
}
@@ -82,7 +92,7 @@ func TestResolve_env(t *testing.T) {
8292
// Env path when no flag path
8393
os.Setenv(keyAgent, "codex")
8494
os.Setenv(keyPath, "/env/codex")
85-
got = Resolve("", "", nil)
95+
got = mustResolve(t, "", "", nil)
8696
if got.CLIPath() != "/env/codex" {
8797
t.Errorf("with CHIEF_AGENT_PATH, CLIPath = %q, want /env/codex", got.CLIPath())
8898
}
@@ -91,13 +101,22 @@ func TestResolve_env(t *testing.T) {
91101
}
92102

93103
func TestResolve_normalize(t *testing.T) {
94-
// Case and spaces
95-
got := Resolve(" CODEX ", "", nil)
104+
got := mustResolve(t, " CODEX ", "", nil)
96105
if got.Name() != "Codex" {
97106
t.Errorf("Resolve(' CODEX ') name = %q, want Codex", got.Name())
98107
}
99108
}
100109

110+
func TestResolve_unknownProvider(t *testing.T) {
111+
_, err := Resolve("typo", "", nil)
112+
if err == nil {
113+
t.Fatal("Resolve(typo) expected error, got nil")
114+
}
115+
if !strings.Contains(err.Error(), "typo") {
116+
t.Errorf("error should mention the bad provider name: %v", err)
117+
}
118+
}
119+
101120
func TestCheckInstalled_notFound(t *testing.T) {
102121
// Use a path that does not exist
103122
p := NewCodexProvider("/nonexistent/codex-binary-that-does-not-exist")
@@ -141,7 +160,7 @@ agent:
141160
if err != nil {
142161
t.Fatal(err)
143162
}
144-
got := Resolve("", "", cfg)
163+
got := mustResolve(t, "", "", cfg)
145164
if got.Name() != "Codex" || got.CLIPath() != "/usr/local/bin/codex" {
146165
t.Errorf("Resolve from config: name=%q path=%q", got.Name(), got.CLIPath())
147166
}

0 commit comments

Comments
 (0)