fix(worker): include stdout in error messages when Claude command fails
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed

Auth errors like "OAuth token has expired" were lost because Claude writes
them to stdout, not stderr. The error message only showed kubectl's generic
"command terminated with exit code 1". Now includes both stdout and stderr
in the error, making failures immediately diagnosable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jordan 2026-02-10 17:55:46 -07:00
parent b7d0e84946
commit cefc15aa7d
2 changed files with 22 additions and 8 deletions

View File

@ -162,6 +162,8 @@ func (a *Adapter) Execute(ctx context.Context, req *domain.AgentRequest, handler
} }
// Determine exit code and error // Determine exit code and error
stdoutStr := finalOutput.String()
stderrStr := stderrOutput.String()
if cmdErr != nil { if cmdErr != nil {
if exitErr, ok := cmdErr.(*exec.ExitError); ok { if exitErr, ok := cmdErr.(*exec.ExitError); ok {
result.ExitCode = exitErr.ExitCode() result.ExitCode = exitErr.ExitCode()
@ -169,17 +171,17 @@ func (a *Adapter) Execute(ctx context.Context, req *domain.AgentRequest, handler
result.ExitCode = 1 result.ExitCode = 1
result.Error = cmdErr result.Error = cmdErr
} }
// Include stderr and troubleshooting help in error // Include stdout, stderr, and troubleshooting help in error
result.Error = a.buildErrorWithHelp(result.Error, stderrOutput.String(), namespace, podName) result.Error = a.buildErrorWithHelp(result.Error, stderrStr, stdoutStr, namespace, podName)
} else if parseErr != nil { } else if parseErr != nil {
result.ExitCode = 1 result.ExitCode = 1
result.Error = a.buildErrorWithHelp(parseErr, stderrOutput.String(), namespace, podName) result.Error = a.buildErrorWithHelp(parseErr, stderrStr, stdoutStr, namespace, podName)
} else if resultMsg != nil && !resultMsg.IsSuccess() { } else if resultMsg != nil && !resultMsg.IsSuccess() {
result.ExitCode = 1 result.ExitCode = 1
if resultMsg.Error != "" { if resultMsg.Error != "" {
result.Error = a.buildErrorWithHelp(fmt.Errorf("%s", resultMsg.Error), stderrOutput.String(), namespace, podName) result.Error = a.buildErrorWithHelp(fmt.Errorf("%s", resultMsg.Error), stderrStr, stdoutStr, namespace, podName)
} else { } else {
result.Error = a.buildErrorWithHelp(nil, stderrOutput.String(), namespace, podName) result.Error = a.buildErrorWithHelp(nil, stderrStr, stdoutStr, namespace, podName)
} }
} }
@ -320,8 +322,8 @@ func (a *Adapter) streamStderrCapture(r io.Reader, handler domain.AgentEventHand
} }
} }
// buildErrorWithHelp creates an error message with stderr output and troubleshooting help. // buildErrorWithHelp creates an error message with captured output and troubleshooting help.
func (a *Adapter) buildErrorWithHelp(err error, stderr, namespace, podName string) error { func (a *Adapter) buildErrorWithHelp(err error, stderr, stdout, namespace, podName string) error {
var msg strings.Builder var msg strings.Builder
if err != nil { if err != nil {
@ -330,6 +332,12 @@ func (a *Adapter) buildErrorWithHelp(err error, stderr, namespace, podName strin
msg.WriteString("claude command failed") msg.WriteString("claude command failed")
} }
// Include stdout if it contains useful output (e.g. auth errors that Claude writes to stdout)
if stdout != "" {
msg.WriteString("\n\noutput:\n")
msg.WriteString(stdout)
}
// Include stderr if available // Include stderr if available
if stderr != "" { if stderr != "" {
msg.WriteString("\n\nstderr:\n") msg.WriteString("\n\nstderr:\n")

View File

@ -221,9 +221,15 @@ func (b *BuildExecutor) Execute(ctx context.Context, task *domain.WorkTask) *dom
} }
} }
// Use streamed output, but fall back to agent's captured output if streaming missed it
output := outputBuilder.String()
if output == "" && agentResult.FinalOutput != "" {
output = agentResult.FinalOutput
}
result := &domain.BuildResult{ result := &domain.BuildResult{
Success: agentResult.Success(), Success: agentResult.Success(),
Output: outputBuilder.String(), Output: output,
DurationMs: time.Since(start).Milliseconds(), DurationMs: time.Since(start).Milliseconds(),
Artifacts: make(map[string]string), Artifacts: make(map[string]string),
} }