diff --git a/internal/engine/gemini/gemini.go b/internal/engine/gemini/gemini.go index e59123d..de13a2c 100644 --- a/internal/engine/gemini/gemini.go +++ b/internal/engine/gemini/gemini.go @@ -76,7 +76,12 @@ func (r *Runner) Run(ctx context.Context, req domain.RunRequest) (domain.RunResu res := domain.RunResult{Role: req.Role, Summary: tail(strings.TrimSpace(string(out)), 40)} if runErr != nil { - res.Err = fmt.Errorf("gemini -p: %w", runErr) + msg := strings.TrimSpace(string(out)) + if msg != "" { + res.Err = fmt.Errorf("gemini -p: %w\n%s", runErr, tail(msg, 20)) + } else { + res.Err = fmt.Errorf("gemini -p: %w", runErr) + } return res, res.Err } return res, nil @@ -116,6 +121,13 @@ func (r *Runner) childEnv(workDir, credsPath string) []string { } else if k := strings.TrimSpace(os.Getenv("AIXGO_VERTEX_API_KEY")); k != "" { env = append(env, "GOOGLE_API_KEY="+k) } + // gemini-cli 0.60+ defaults folder trust on; headless CI checkouts are + // untrusted and exit 55 (FatalUntrustedWorkspaceError). -y does not bypass + // that gate — mirror unattended CI by trusting the workspace unless the + // caller already set the override. + if !hasEnvKey(r.ExtraEnv, "GEMINI_CLI_TRUST_WORKSPACE") { + env = append(env, "GEMINI_CLI_TRUST_WORKSPACE=true") + } return env } diff --git a/internal/engine/gemini/gemini_test.go b/internal/engine/gemini/gemini_test.go index 9e44b3d..d63e5a6 100644 --- a/internal/engine/gemini/gemini_test.go +++ b/internal/engine/gemini/gemini_test.go @@ -172,6 +172,40 @@ func TestRunDefaultsLocationWhenUnset(t *testing.T) { } } +const echoTrustEnv = `#!/bin/sh +printf '%s\n' "$GEMINI_CLI_TRUST_WORKSPACE" +exit 0 +` + +func TestRunTrustsWorkspaceForHeadlessCI(t *testing.T) { + r := New() + r.Bin = writeFakeGemini(t, echoTrustEnv) + res, err := r.Run(context.Background(), domain.RunRequest{ + Role: domain.RoleImplementer, WorkDir: t.TempDir(), Prompt: "x", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.Summary != "true" { + t.Fatalf("GEMINI_CLI_TRUST_WORKSPACE = %q, want true", res.Summary) + } +} + +func TestRunPreservesCallerTrustWorkspaceOverride(t *testing.T) { + r := New() + r.Bin = writeFakeGemini(t, echoTrustEnv) + r.ExtraEnv = []string{"GEMINI_CLI_TRUST_WORKSPACE=false"} + res, err := r.Run(context.Background(), domain.RunRequest{ + Role: domain.RoleImplementer, WorkDir: t.TempDir(), Prompt: "x", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.Summary != "false" { + t.Fatalf("caller override lost: %q", res.Summary) + } +} + func TestRunReportsAFailureAsAnEngineError(t *testing.T) { r := New() r.Bin = writeFakeGemini(t, "#!/bin/sh\necho boom >&2\nexit 3\n") @@ -184,6 +218,26 @@ func TestRunReportsAFailureAsAnEngineError(t *testing.T) { if !strings.Contains(res.Summary, "boom") { t.Fatalf("the CLI's own output should be kept: %q", res.Summary) } + if !strings.Contains(err.Error(), "boom") { + t.Fatalf("engine error should include CLI output: %v", err) + } +} + +func TestRunReportsSilentFailureAsAnEngineError(t *testing.T) { + r := New() + r.Bin = writeFakeGemini(t, "#!/bin/sh\nexit 3\n") + res, err := r.Run(context.Background(), domain.RunRequest{ + Role: domain.RoleImplementer, WorkDir: t.TempDir(), Prompt: "x", + }) + if err == nil || res.Err == nil { + t.Fatal("a non-zero exit must surface as an engine error") + } + if !strings.Contains(err.Error(), "exit status 3") { + t.Fatalf("silent failure error = %v", err) + } + if strings.Contains(err.Error(), "\n") { + t.Fatalf("empty CLI output should not add a detail line: %v", err) + } } func TestRunRequiresAWorkDir(t *testing.T) {