Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 28 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,23 @@ fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))
- **NEVER** use `fmt.Println()` or `fmt.Printf()` directly - all output should go to stderr
- Use console formatting helpers with `os.Stderr` for consistent styling
- For simple messages without console formatting: `fmt.Fprintf(os.Stderr, "message\n")`
- **Exception**: Structured output (JSON, hashes, graphs) goes to stdout for piping/redirection

**Examples:**
```go
// ✅ CORRECT - Diagnostic output to stderr
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Processing..."))
fmt.Fprintf(os.Stderr, "Warning: %s\n", msg)

// ✅ CORRECT - Structured output to stdout
fmt.Println(string(jsonBytes)) // JSON output
fmt.Println(hash) // Hash output
fmt.Println(mermaidGraph) // Graph output

// ❌ INCORRECT - Diagnostic output to stdout
fmt.Println("Processing...") // Should use stderr
fmt.Printf("Warning: %s\n", msg) // Should use stderr
```

## Debug Logging

Expand Down Expand Up @@ -525,18 +542,26 @@ if err != nil {
### Console Output Requirements

```go
// ✅ CORRECT - All output to stderr with console formatting
// ✅ CORRECT - All diagnostic output to stderr with console formatting
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Compiled successfully"))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Processing workflow..."))
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("File has changes"))
fmt.Fprintln(os.Stderr, console.FormatErrorMessage(err.Error()))

// ❌ INCORRECT - stdout, no formatting
// ✅ CORRECT - Structured output to stdout for piping/redirection
fmt.Println(string(jsonBytes)) // JSON output
fmt.Println(hash) // Hash output
fmt.Println(mermaidGraph) // Graph output

// ❌ INCORRECT - Diagnostic output to stdout, no formatting
fmt.Println("Success")
fmt.Printf("Status: %s\n", status)
```

**Exception**: JSON output goes to stdout, all other output to stderr
**Output Routing Rules (Unix Conventions):**
- **Diagnostic output** (messages, warnings, errors) → `stderr`
- **Structured data** (JSON, hashes, graphs) → `stdout`
- **Rationale**: Allows users to pipe/redirect data without diagnostic noise

### Help Text Standards

Expand Down
2 changes: 1 addition & 1 deletion cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ var versionCmd = &cobra.Command{
Use: "version",
Short: "Show gh aw extension version information",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("%s version %s\n", string(constants.CLIExtensionPrefix), version)
fmt.Fprintf(os.Stderr, "%s version %s\n", string(constants.CLIExtensionPrefix), version)
return nil
},
}
Expand Down
3 changes: 2 additions & 1 deletion cmd/gh-aw/main_entry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,8 @@ func TestMainFunctionExecutionPath(t *testing.T) {
cmd.Dir = "."

// This should run successfully (exit code 0) even if no workflows found
output, err := cmd.Output()
// Use CombinedOutput to capture both stdout and stderr (version now outputs to stderr)
output, err := cmd.CombinedOutput()
if err != nil {
// Check if it's just a non-zero exit (which is okay for some commands)
if exitError, ok := err.(*exec.ExitError); ok {
Expand Down
5 changes: 3 additions & 2 deletions pkg/workflow/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"os"
"strings"

"github.com/githubnext/gh-aw/pkg/logger"
Expand Down Expand Up @@ -238,7 +239,7 @@ func generateCacheSteps(builder *strings.Builder, data *WorkflowData, verbose bo
var topLevel map[string]any
if err := yaml.Unmarshal([]byte(data.Cache), &topLevel); err != nil {
if verbose {
fmt.Printf("Warning: Failed to parse cache configuration: %v\n", err)
fmt.Fprintf(os.Stderr, "Warning: Failed to parse cache configuration: %v\n", err)
}
return
}
Expand All @@ -247,7 +248,7 @@ func generateCacheSteps(builder *strings.Builder, data *WorkflowData, verbose bo
cacheConfig, exists := topLevel["cache"]
if !exists {
if verbose {
fmt.Printf("Warning: No cache key found in parsed configuration\n")
fmt.Fprintf(os.Stderr, "Warning: No cache key found in parsed configuration\n")
}
return
}
Expand Down
13 changes: 7 additions & 6 deletions pkg/workflow/claude_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package workflow
import (
"encoding/json"
"fmt"
"os"
"strings"
"time"

Expand Down Expand Up @@ -149,7 +150,7 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
// If that fails, try to parse as mixed format (debug logs + JSONL)
claudeLogsLog.Print("JSON array parse failed, trying JSONL format")
if verbose {
fmt.Printf("Failed to parse Claude log as JSON array, trying JSONL format: %v\n", err)
fmt.Fprintf(os.Stderr, "Failed to parse Claude log as JSON array, trying JSONL format: %v\n", err)
}

logEntries = []map[string]any{}
Expand Down Expand Up @@ -208,7 +209,7 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
if err := json.Unmarshal([]byte(trimmedLine), &jsonEntry); err != nil {
// Skip invalid JSON lines (could be partial debug output)
if verbose {
fmt.Printf("Skipping invalid JSON line: %s\n", trimmedLine)
fmt.Fprintf(os.Stderr, "Skipping invalid JSON line: %s\n", trimmedLine)
}
continue
}
Expand All @@ -218,13 +219,13 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe

if len(logEntries) == 0 {
if verbose {
fmt.Printf("No valid JSON entries found in Claude log\n")
fmt.Fprintf(os.Stderr, "No valid JSON entries found in Claude log\n")
}
return metrics
}

if verbose {
fmt.Printf("Extracted %d JSON entries from mixed format Claude log\n", len(logEntries))
fmt.Fprintf(os.Stderr, "Extracted %d JSON entries from mixed format Claude log\n", len(logEntries))
}
}

Expand Down Expand Up @@ -276,7 +277,7 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
}

if verbose {
fmt.Printf("Extracted from Claude result payload: tokens=%d, cost=%.4f, turns=%d\n",
fmt.Fprintf(os.Stderr, "Extracted from Claude result payload: tokens=%d, cost=%.4f, turns=%d\n",
metrics.TokenUsage, metrics.EstimatedCost, metrics.Turns)
}
break
Expand Down Expand Up @@ -319,7 +320,7 @@ func (e *ClaudeEngine) parseClaudeJSONLog(logContent string, verbose bool) LogMe
for _, seq := range metrics.ToolSequences {
totalTools += len(seq)
}
fmt.Printf("Claude parser extracted %d tool sequences with %d total tool calls\n",
fmt.Fprintf(os.Stderr, "Claude parser extracted %d tool sequences with %d total tool calls\n",
len(metrics.ToolSequences), totalTools)
}

Expand Down
5 changes: 3 additions & 2 deletions pkg/workflow/engine_firewall_support.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"os"

"github.com/githubnext/gh-aw/pkg/console"
"github.com/githubnext/gh-aw/pkg/logger"
Expand Down Expand Up @@ -71,7 +72,7 @@ func (c *Compiler) checkNetworkSupport(engine CodingAgentEngine, networkPermissi
}

// In non-strict mode, emit a warning
fmt.Println(console.FormatWarningMessage(message))
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(message))
c.IncrementWarningCount()

return nil
Expand Down Expand Up @@ -99,7 +100,7 @@ func (c *Compiler) checkFirewallDisable(engine CodingAgentEngine, networkPermiss
}

// In non-strict mode, emit a warning
fmt.Println(console.FormatWarningMessage(message))
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(message))
c.IncrementWarningCount()
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/mcp_config_custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"os"
"sort"
"strings"

Expand Down Expand Up @@ -122,7 +123,7 @@ func renderSharedMCPConfig(yaml *strings.Builder, toolName string, toolConfig ma
}
}
default:
fmt.Println(console.FormatWarningMessage(fmt.Sprintf("Custom MCP server '%s' has unsupported type '%s'. Supported types: stdio, http", toolName, mcpType)))
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Custom MCP server '%s' has unsupported type '%s'. Supported types: stdio, http", toolName, mcpType)))
return nil
}

Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/mcp_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"os"
"sort"
"strings"
"time"
Expand Down Expand Up @@ -489,7 +490,7 @@ func HandleCustomMCPToolInSwitch(
if toolConfig, ok := tools[toolName].(map[string]any); ok {
if hasMcp, _ := hasMCPConfig(toolConfig); hasMcp {
if err := renderFunc(yaml, toolName, toolConfig, isLast); err != nil {
fmt.Printf("Error generating custom MCP configuration for %s: %v\n", toolName, err)
fmt.Fprintf(os.Stderr, "Error generating custom MCP configuration for %s: %v\n", toolName, err)
}
return true
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/workflow/push_to_pull_request_branch.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"os"

"github.com/githubnext/gh-aw/pkg/logger"
)
Expand Down Expand Up @@ -68,7 +69,7 @@ func (c *Compiler) parsePushToPullRequestBranchConfig(outputMap map[string]any)
default:
// Invalid value, use default and log warning
if c.verbose {
fmt.Printf("Warning: invalid if-no-changes value '%s', using default 'warn'\n", ifNoChangesStr)
fmt.Fprintf(os.Stderr, "Warning: invalid if-no-changes value '%s', using default 'warn'\n", ifNoChangesStr)
}
pushToBranchConfig.IfNoChanges = "warn"
}
Expand Down
18 changes: 9 additions & 9 deletions pkg/workflow/stop_after.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,16 @@ func (c *Compiler) processStopAfterConfiguration(frontmatter map[string]any, wor
stopAfterLog.Printf("Resolved stop time from %s to %s", originalStopTime, resolvedStopTime)

if c.verbose && isRelativeStopTime(originalStopTime) {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Refreshed relative stop-after to: %s", resolvedStopTime)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Refreshed relative stop-after to: %s", resolvedStopTime)))
} else if c.verbose && originalStopTime != resolvedStopTime {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Refreshed absolute stop-after from '%s' to: %s", originalStopTime, resolvedStopTime)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Refreshed absolute stop-after from '%s' to: %s", originalStopTime, resolvedStopTime)))
}
} else if existingStopTime != "" {
// Preserve existing stop time during recompilation (default behavior)
stopAfterLog.Printf("Preserving existing stop time from lock file: %s", existingStopTime)
workflowData.StopTime = existingStopTime
if c.verbose {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Preserving existing stop time from lock file: %s", existingStopTime)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Preserving existing stop time from lock file: %s", existingStopTime)))
}
} else {
// First compilation or no existing stop time, generate new one
Expand All @@ -100,9 +100,9 @@ func (c *Compiler) processStopAfterConfiguration(frontmatter map[string]any, wor
workflowData.StopTime = resolvedStopTime

if c.verbose && isRelativeStopTime(originalStopTime) {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Resolved relative stop-after to: %s", resolvedStopTime)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Resolved relative stop-after to: %s", resolvedStopTime)))
} else if c.verbose && originalStopTime != resolvedStopTime {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Parsed absolute stop-after from '%s' to: %s", originalStopTime, resolvedStopTime)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Parsed absolute stop-after from '%s' to: %s", originalStopTime, resolvedStopTime)))
}
}
}
Expand Down Expand Up @@ -329,9 +329,9 @@ func (c *Compiler) processSkipIfMatchConfiguration(frontmatter map[string]any, w

if c.verbose && workflowData.SkipIfMatch != nil {
if workflowData.SkipIfMatch.Max == 1 {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Skip-if-match query configured: %s (max: 1 match)", workflowData.SkipIfMatch.Query)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skip-if-match query configured: %s (max: 1 match)", workflowData.SkipIfMatch.Query)))
} else {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Skip-if-match query configured: %s (max: %d matches)", workflowData.SkipIfMatch.Query, workflowData.SkipIfMatch.Max)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skip-if-match query configured: %s (max: %d matches)", workflowData.SkipIfMatch.Query, workflowData.SkipIfMatch.Max)))
}
}

Expand All @@ -349,9 +349,9 @@ func (c *Compiler) processSkipIfNoMatchConfiguration(frontmatter map[string]any,

if c.verbose && workflowData.SkipIfNoMatch != nil {
if workflowData.SkipIfNoMatch.Min == 1 {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Skip-if-no-match query configured: %s (min: 1 match)", workflowData.SkipIfNoMatch.Query)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skip-if-no-match query configured: %s (min: 1 match)", workflowData.SkipIfNoMatch.Query)))
} else {
fmt.Println(console.FormatInfoMessage(fmt.Sprintf("Skip-if-no-match query configured: %s (min: %d matches)", workflowData.SkipIfNoMatch.Query, workflowData.SkipIfNoMatch.Min)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skip-if-no-match query configured: %s (min: %d matches)", workflowData.SkipIfNoMatch.Query, workflowData.SkipIfNoMatch.Min)))
}
}

Expand Down
Loading