diff --git a/pkg/cli/logs_github_api.go b/pkg/cli/logs_github_api.go index 263a716d7f..d3cfa59921 100644 --- a/pkg/cli/logs_github_api.go +++ b/pkg/cli/logs_github_api.go @@ -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 diff --git a/pkg/cli/logs_orchestrator.go b/pkg/cli/logs_orchestrator.go index 545d1628e4..fc91026af9 100644 --- a/pkg/cli/logs_orchestrator.go +++ b/pkg/cli/logs_orchestrator.go @@ -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 { diff --git a/pkg/cli/logs_report.go b/pkg/cli/logs_report.go index 77e749f8fc..c3dfd771e2 100644 --- a/pkg/cli/logs_report.go +++ b/pkg/cli/logs_report.go @@ -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) @@ -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 { @@ -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 { @@ -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 @@ -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)) + } }