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
4 changes: 2 additions & 2 deletions pkg/cli/logs_github_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,9 @@ func listWorkflowRunsWithPagination(workflowName string, limit int, startDate, e
return nil, 0, fmt.Errorf("failed to parse workflow runs: %w", err)
}

// Stop spinner with success message
// Stop spinner silently - don't show per-iteration messages
if !verbose {
spinner.StopWithMessage(fmt.Sprintf("✓ Fetched %d workflow runs", len(runs)))
spinner.Stop()
}

// Store the total count fetched from API before filtering
Expand Down
11 changes: 1 addition & 10 deletions pkg/cli/logs_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -729,18 +729,9 @@ func downloadRunArtifactsConcurrent(ctx context.Context, runs []WorkflowRun, out
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Download interrupted: %v", err)))
}

// Clear progress bar and show final message
// Clear progress bar silently - detailed summary shown at the end
if progressBar != nil {
fmt.Fprint(os.Stderr, "\r\033[K") // Clear the line
successCount := 0
for _, result := range results {
// Count as successful if: no error AND not skipped
// This includes both newly downloaded and cached runs
if result.Error == nil && !result.Skipped {
successCount++
}
}
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Processed %d/%d runs successfully", successCount, totalRuns)))
}

if verbose {
Expand Down
155 changes: 152 additions & 3 deletions pkg/cli/logs_report.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,54 @@ func buildLogsData(processedRuns []ProcessedRun, outputDir string, continuation
}
}

// isValidToolName checks if a tool name appears to be valid
// Filters out single words, common words, and other garbage that shouldn't be tools
func isValidToolName(toolName string) bool {
name := strings.TrimSpace(toolName)

// Filter out empty names
if name == "" || name == "-" {
return false
}

// Filter out single character names
if len(name) == 1 {
return false
}

// Filter out common English words that are likely from error messages
commonWords := map[string]bool{
"calls": true, "to": true, "for": true, "the": true, "a": true, "an": true,
"is": true, "are": true, "was": true, "were": true, "be": true, "been": true,
"have": true, "has": true, "had": true, "do": true, "does": true, "did": true,
"will": true, "would": true, "could": true, "should": true, "may": true, "might": true,
"Testing": true, "multiple": true, "launches": true, "command": true, "invocation": true,
"with": true, "from": true, "by": true, "at": true, "in": true, "on": true,
}

if commonWords[name] {
return false
}

// Tool names should typically contain underscores, hyphens, or be camelCase
// or be all lowercase. Single words without these patterns are suspect.
hasUnderscore := strings.Contains(name, "_")
hasHyphen := strings.Contains(name, "-")
hasCapital := strings.ToLower(name) != name

// If it's a single word with no underscores/hyphens and is lowercase and short,
// it's likely a fragment
words := strings.Fields(name)
if len(words) == 1 && !hasUnderscore && !hasHyphen && len(name) < 10 && !hasCapital {
// Could be a fragment - be conservative and reject if it's a common word
return false
}

return true
}

// buildToolUsageSummary aggregates tool usage across all runs
// Filters out invalid tool names that appear to be fragments or garbage
func buildToolUsageSummary(processedRuns []ProcessedRun) []ToolUsageSummary {
toolStats := make(map[string]*ToolUsageSummary)

Expand All @@ -251,6 +298,12 @@ func buildToolUsageSummary(processedRuns []ProcessedRun) []ToolUsageSummary {

for _, toolCall := range metrics.ToolCalls {
displayKey := workflow.PrettifyToolName(toolCall.Name)

// Filter out invalid tool names
if !isValidToolName(displayKey) {
continue
}

toolRunTracker[displayKey] = true

if existing, exists := toolStats[displayKey]; exists {
Expand Down Expand Up @@ -677,7 +730,71 @@ func aggregateLogErrors(processedRuns []ProcessedRun, agg logErrorAggregator) []
return results
}

// isActionableError checks if an error message is actionable (user-relevant)
// Returns false for internal debug messages, validation logs, JSON fragments, etc.
func isActionableError(message string) bool {
msg := strings.ToLower(message)

// Filter out internal validation/debug messages
debugPatterns := []string{
"validation completed",
"executePromptDirectly",
"starting validate_errors",
"loaded", "error patterns",
"pattern ", "/16:", // Pattern testing logs
"validation completed in",
"starting error validation",
"error validation completed",
"const { main }",
"require(",
"perfect! the",
"failed as expected",
}

for _, pattern := range debugPatterns {
if strings.Contains(msg, pattern) {
return false
}
}

// Filter out JSON fragments and data structures
jsonPatterns := []string{
`"errorCodesToRetry"`,
`"description":`,
`"statement":`,
`"content":`,
`"onRequestError"`,
`[{`, `}]`, `"[`,
}

for _, pattern := range jsonPatterns {
if strings.Contains(message, pattern) {
return false
}
}

// Filter out MCP server logs (stderr output)
if strings.Contains(msg, "[mcp server") ||
strings.Contains(msg, "[safeoutputs]") ||
strings.Contains(msg, "send: {\"jsonrpc\"") {
return false
}

// Filter out Squid proxy logs
if strings.Contains(msg, "::1:") && strings.Contains(msg, "NONE_NONE:HIER_NONE") {
return false
}

// Filter out tool invocation result logs (these are outputs, not errors)
if strings.HasPrefix(msg, "tool invocation result:") {
return false
}

return true
}

// buildCombinedErrorsSummary aggregates errors and warnings across all runs into a single list
// Filters out non-actionable errors like debug logs, JSON fragments, and internal messages
func buildCombinedErrorsSummary(processedRuns []ProcessedRun) []ErrorSummary {
agg := logErrorAggregator{
generateKey: func(logErr workflow.LogError) string {
Expand All @@ -697,7 +814,23 @@ func buildCombinedErrorsSummary(processedRuns []ProcessedRun) []ErrorSummary {
},
}

return aggregateLogErrors(processedRuns, agg)
allErrors := aggregateLogErrors(processedRuns, agg)

// Filter out non-actionable errors
var actionableErrors []ErrorSummary
for _, err := range allErrors {
if isActionableError(err.Message) {
actionableErrors = append(actionableErrors, err)
}
}

// Limit to top 20 most common errors to keep output concise
maxErrors := 20
if len(actionableErrors) > maxErrors {
actionableErrors = actionableErrors[:maxErrors]
}

return actionableErrors
}

// buildErrorsSummary aggregates errors and warnings across all runs
Expand Down Expand Up @@ -786,6 +919,22 @@ func renderLogsConsole(data LogsData) {
// Use unified console rendering for the entire logs data structure
fmt.Print(console.RenderStruct(data))

// Display logs location
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("Downloaded %d logs to %s", data.Summary.TotalRuns, data.LogsLocation)))
// Display concise summary at the end
fmt.Fprintln(os.Stderr, "") // Blank line for spacing
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage(fmt.Sprintf("✓ Downloaded %d workflow logs to %s", data.Summary.TotalRuns, data.LogsLocation)))

// Show key metrics in a concise format
if data.Summary.TotalErrors > 0 || data.Summary.TotalWarnings > 0 {
fmt.Fprintf(os.Stderr, " %s %d errors, %d warnings across %d runs\n",
console.FormatInfoMessage("•"),
data.Summary.TotalErrors,
data.Summary.TotalWarnings,
data.Summary.TotalRuns)
}

if len(data.ToolUsage) > 0 {
fmt.Fprintf(os.Stderr, " %s %d unique tools used\n",
console.FormatInfoMessage("•"),
len(data.ToolUsage))
}
}
Loading