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
50 changes: 50 additions & 0 deletions pkg/cli/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ type AuditOptions struct {
ArtifactSets []string
ExperimentFilter string
VariantFilter string
EvalsOnly bool
}

var auditCommandLong = `Audit one or more workflow runs by downloading artifacts and logs, detecting errors,
Expand Down Expand Up @@ -82,6 +83,7 @@ type auditCommandOptions struct {
stdin bool
experimentFilter string
variantFilter string
evalsOnly bool
}

// NewAuditCommand creates the audit command
Expand Down Expand Up @@ -109,6 +111,7 @@ func registerAuditCommandFlags(cmd *cobra.Command) {
cmd.Flags().Bool("stdin", false, "Read workflow run IDs or URLs from stdin (one per line) instead of positional arguments")
cmd.Flags().String("experiment", "", "Filter to runs that include this experiment name")
cmd.Flags().String("variant", "", "Filter to runs with a specific variant value (requires --experiment)")
cmd.Flags().Bool("evals", false, "Skip runs that do not contain evals results (evals.jsonl); automatically downloads the evals artifact when --artifacts is narrowed")
RegisterDirFlagCompletion(cmd, "output")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/grill-with-docs] The flag description says "Include evals results in audit report" but its primary effect is filtering (skipping runs without evals). This is more consistent with the logs --evals description ("Filter to runs containing evals results"). A misleading description can confuse users expecting the flag to add something to the report rather than exclude runs.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 53a7bb7 — the audit --evals flag description now accurately says "Skip runs that do not contain evals results (evals.jsonl); automatically downloads the evals artifact when --artifacts is narrowed".

}

Expand All @@ -124,6 +127,12 @@ func runAuditCommand(cmd *cobra.Command, args []string) error {
if len(args) == 1 {
return runAuditSingle(cmd.Context(), args[0], opts)
}
if opts.evalsOnly {
return errors.New(console.FormatErrorWithSuggestions(
"--evals is not supported in multi-run diff mode",
[]string{"Provide a single run ID with --evals to filter by evals results"},
))
}
return runAuditMulti(cmd.Context(), args, opts.repoFlag, opts.outputDir, opts.verbose, opts.jsonOutput, opts.format, opts.artifacts)
}

Expand All @@ -139,12 +148,21 @@ func getAuditCommandOptions(cmd *cobra.Command) (auditCommandOptions, error) {
opts.stdin, _ = cmd.Flags().GetBool("stdin")
opts.experimentFilter, _ = cmd.Flags().GetString("experiment")
opts.variantFilter, _ = cmd.Flags().GetString("variant")
opts.evalsOnly, _ = cmd.Flags().GetBool("evals")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 04309d3runAuditCommand now returns a clear error message when --evals is combined with multiple run IDs, directing users to single-run mode.

if opts.variantFilter != "" && opts.experimentFilter == "" {
return auditCommandOptions{}, errors.New(console.FormatErrorWithSuggestions(
"--variant requires --experiment to be specified",
[]string{"Add --experiment <name> to filter by experiment name alongside --variant"},
))
}
// Auto-include the evals artifact when --evals is specified and the user has
// narrowed the artifact set (non-empty --artifacts). When --artifacts is empty
// the default is "all", which already includes every artifact including evals,
// so we must not append here: doing so would change the default from "all" to
// "evals-only" and omit the activation/agent artifacts required for a full report.
if len(opts.artifacts) > 0 {
opts.artifacts = applyEvalsArtifact(opts.artifacts, opts.evalsOnly)
}
return opts, nil
}

Expand Down Expand Up @@ -199,6 +217,7 @@ func runAuditSingle(ctx context.Context, runIDOrURL string, opts auditCommandOpt
ArtifactSets: opts.artifacts,
ExperimentFilter: opts.experimentFilter,
VariantFilter: opts.variantFilter,
EvalsOnly: opts.evalsOnly,
})
}

Expand Down Expand Up @@ -302,6 +321,7 @@ type auditRunConfig struct {
artifactFilter []string
experimentFilter string
variantFilter string
evalsOnly bool
}

type auditAnalysisResults struct {
Expand Down Expand Up @@ -355,6 +375,9 @@ func AuditWorkflowRun(ctx context.Context, runID int64, opts AuditOptions) error
if shouldSkipAuditRun(cfg.runID, cfg.outputDir, cfg.experimentFilter, cfg.variantFilter) {
return nil
}
if shouldSkipForEvals(cfg) {
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The evals-skip block (lines 373-377) is copy-pasted verbatim into renderCachedAuditIfAvailable (lines 493-497), including the duplicate auditLog.Printf + fmt.Fprintf(os.Stderr, ...) pair. Divergence is likely as the feature evolves.

💡 Suggestion

Extract a small helper used by both:

// shouldSkipForEvals returns true and logs when no evals results are present.
func shouldSkipForEvals(cfg auditRunConfig) bool {
    if !cfg.evalsOnly {
        return false
    }
    if runHasEvals(cfg.outputDir, cfg.verbose) {
        return false
    }
    auditLog.Printf("Skipping run %d: no evals results found (filtered by --evals)", cfg.runID)
    fmt.Fprintf(os.Stderr, "%s\n", console.FormatInfoMessage(
        fmt.Sprintf("Skipping run %d: workflow does not have evals results (filtered by --evals)", cfg.runID)))
    return true
}

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 53a7bb7 — extracted shouldSkipForEvals(cfg auditRunConfig) bool at the bottom of audit.go. Both AuditWorkflowRun and renderCachedAuditIfAvailable (which has a distinct "bypass cache" path) use this single helper. The helper includes the verbose guard for the stderr message.

}
return renderAuditReport(ctx, processedRun, results.metrics, results.mcpToolUsage, cfg.auditOptions())
}

Expand All @@ -376,6 +399,7 @@ func newAuditRunConfig(runID int64, opts AuditOptions) (auditRunConfig, error) {
artifactFilter: ResolveArtifactFilter(opts.ArtifactSets),
experimentFilter: opts.ExperimentFilter,
variantFilter: opts.VariantFilter,
evalsOnly: opts.EvalsOnly,
}, nil
}

Expand Down Expand Up @@ -454,6 +478,7 @@ func (cfg auditRunConfig) auditOptions() AuditOptions {
Verbose: cfg.verbose,
Parse: cfg.parse,
JSONOutput: cfg.jsonOutput,
EvalsOnly: cfg.evalsOnly,
}
}

Expand All @@ -469,6 +494,14 @@ func renderCachedAuditIfAvailable(ctx context.Context, cfg auditRunConfig) (bool
if shouldSkipAuditRun(cfg.runID, cfg.outputDir, cfg.experimentFilter, cfg.variantFilter) {
return true, nil
}
// When --evals is set but the evals artifact is not present locally, the cache
// was created without evals (e.g., default usage-only download). Bypass the
// cache so prepareAuditWorkflowRun can fetch the evals artifact; the filter at
// the post-download check will then correctly decide whether to skip the run.
if cfg.evalsOnly && !runHasEvals(cfg.outputDir, cfg.verbose) {
auditLog.Printf("Cache miss for run %d evals: evals artifact not present locally, bypassing cache", cfg.runID)
return false, nil
}
processedRun := processedRunFromSummary(summary, cfg.outputDir)
return true, renderAuditReport(ctx, processedRun, summary.Metrics, summary.MCPToolUsage, cfg.auditOptions())
}
Expand Down Expand Up @@ -931,3 +964,20 @@ func renderAuditCompletion(runOutputDir string, jsonOutput bool) {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("Audit complete. Logs saved to "+absOutputDir))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Tip: use --artifacts to select specific artifact sets (agent, firewall, mcp, activation, detection, etc.)"))
}

// shouldSkipForEvals returns true when evals filtering is active but no evals results
// are found locally after download. It logs the skip decision and, when verbose, prints
// an info message to stderr. Call this only after artifact download has completed.
func shouldSkipForEvals(cfg auditRunConfig) bool {
if !cfg.evalsOnly {
return false
}
if runHasEvals(cfg.outputDir, cfg.verbose) {
return false
}
auditLog.Printf("Skipping run %d: no evals results found (filtered by --evals)", cfg.runID)
if cfg.verbose {
fmt.Fprintf(os.Stderr, "%s\n", console.FormatInfoMessage(fmt.Sprintf("Skipping run %d: workflow does not have evals results (filtered by --evals)", cfg.runID)))
}
return true
}
22 changes: 22 additions & 0 deletions pkg/cli/logs_artifact_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ const (
// ArtifactSetUsage downloads the compact usage artifact produced by the
// conclusion job (aw-info.jsonl, usage summaries, token usage JSONL).
ArtifactSetUsage ArtifactSet = "usage"

// ArtifactSetEvals downloads the evals artifact containing BinEval evaluation
// results (evals.jsonl) produced by the evals job.
ArtifactSetEvals ArtifactSet = "evals"
)

// artifactSetArtifacts maps each named set to the list of artifact base names it includes.
Expand All @@ -87,6 +91,8 @@ var artifactSetArtifacts = map[ArtifactSet][]string{
ArtifactSetExperiment: {constants.ExperimentArtifactName},
// usage: compact conclusion artifact for lightweight reporting/forecasting.
ArtifactSetUsage: {constants.UsageArtifactName},
// evals: BinEval evaluation results uploaded by the evals job.
ArtifactSetEvals: {constants.EvalsArtifactName},
}

const maxArtifactHintExamples = 2
Expand Down Expand Up @@ -276,3 +282,19 @@ func findMissingFilterEntries(filter []string, outputDir string) []string {
}
return missing
}

// applyEvalsArtifact appends the evals artifact set to artifacts when evalsOnly is true
// and neither ArtifactSetEvals nor ArtifactSetAll is already present. This ensures
// evals.jsonl is downloaded without requiring the user to also pass --artifacts evals.
//
// Note: callers that treat an empty artifacts slice as "all" (e.g., the audit command)
// should guard with len(artifacts) > 0 before calling this function, to avoid
// changing the empty/"all" default into an evals-only download.
func applyEvalsArtifact(artifacts []string, evalsOnly bool) []string {
if evalsOnly &&
!slices.Contains(artifacts, string(ArtifactSetEvals)) &&
!slices.Contains(artifacts, string(ArtifactSetAll)) {
return append(artifacts, string(ArtifactSetEvals))
}
return artifacts
}
2 changes: 1 addition & 1 deletion pkg/cli/logs_artifact_set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ func TestValidArtifactSetNames(t *testing.T) {
names := ValidArtifactSetNames()
require.NotEmpty(t, names, "ValidArtifactSetNames should return non-empty slice")

expected := []string{"all", "activation", "agent", "detection", "experiment", "firewall", "github-api", "mcp", "usage"}
expected := []string{"all", "activation", "agent", "detection", "evals", "experiment", "firewall", "github-api", "mcp", "usage"}
assert.ElementsMatch(t, expected, names, "ValidArtifactSetNames should contain all known sets")
}

Expand Down
10 changes: 10 additions & 0 deletions pkg/cli/logs_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Downloaded artifacts include (when using --artifacts all):
` + string(constants.CLIExtensionPrefix) + ` logs --ref main # Filter logs by branch or tag
` + string(constants.CLIExtensionPrefix) + ` logs --ref feature-xyz # Filter logs by feature branch
` + string(constants.CLIExtensionPrefix) + ` logs --filtered-integrity # Filter logs containing items that were filtered by gateway integrity checks
` + string(constants.CLIExtensionPrefix) + ` logs --evals # Filter logs from workflows with evals results
` + string(constants.CLIExtensionPrefix) + ` logs --exclude-staged # Exclude staged workflow runs from results

# Run ID range filtering
Expand Down Expand Up @@ -152,6 +153,7 @@ Downloaded artifacts include (when using --artifacts all):
summaryFile, _ := cmd.Flags().GetString("summary-file")
safeOutputType, _ := cmd.Flags().GetString("safe-output")
filteredIntegrity, _ := cmd.Flags().GetBool("filtered-integrity")
evalsOnly, _ := cmd.Flags().GetBool("evals")
train, _ := cmd.Flags().GetBool("train")
format, _ := cmd.Flags().GetString("format")
reportFile, _ := cmd.Flags().GetString("report-file")
Expand All @@ -170,6 +172,8 @@ Downloaded artifacts include (when using --artifacts all):
return err

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The --evals auto-include logic (lines 173-175 and 333-335 in the stdin path) is duplicated verbatim. This pattern already exists once for the audit command too, so it will need to be kept in sync in three places.

💡 Suggestion

Extract a small helper or move the auto-include into getAuditCommandOptions-style resolution that happens once, for example:

func applyEvalsArtifact(artifacts []string, evalsOnly bool) []string {
    if evalsOnly && !slices.Contains(artifacts, string(ArtifactSetEvals)) && !slices.Contains(artifacts, string(ArtifactSetAll)) {
        return append(artifacts, string(ArtifactSetEvals))
    }
    return artifacts
}

Then call artifacts = applyEvalsArtifact(artifacts, evalsOnly) in both command handlers and in getAuditCommandOptions.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 53a7bb7 — added applyEvalsArtifact(artifacts []string, evalsOnly bool) []string to logs_artifact_set.go. Both duplicate blocks in logs_command.go now call artifacts = applyEvalsArtifact(artifacts, evalsOnly), and getAuditCommandOptions in audit.go simplifies to opts.artifacts = applyEvalsArtifact(opts.artifacts, opts.evalsOnly) (guarded by the existing len > 0 check).

}

artifacts = applyEvalsArtifact(artifacts, evalsOnly)

return DownloadWorkflowLogsFromStdin(cmd.Context(), StdinLogsOptions{
RunURLs: runURLs,
OutputDir: outputDir,
Expand All @@ -186,6 +190,7 @@ Downloaded artifacts include (when using --artifacts all):
SummaryFile: summaryFile,
SafeOutputType: safeOutputType,
FilteredIntegrity: filteredIntegrity,
EvalsOnly: evalsOnly,
Train: train,
Format: format,
ReportFile: reportFile,
Expand Down Expand Up @@ -273,6 +278,7 @@ Downloaded artifacts include (when using --artifacts all):
summaryFile, _ := cmd.Flags().GetString("summary-file")
safeOutputType, _ := cmd.Flags().GetString("safe-output")
filteredIntegrity, _ := cmd.Flags().GetBool("filtered-integrity")
evalsOnly, _ := cmd.Flags().GetBool("evals")
train, _ := cmd.Flags().GetBool("train")
format, _ := cmd.Flags().GetString("format")
reportFile, _ := cmd.Flags().GetString("report-file")
Expand Down Expand Up @@ -321,6 +327,8 @@ Downloaded artifacts include (when using --artifacts all):

logsCommandLog.Printf("Executing logs download: workflow=%s, count=%d, engine=%s, train=%v, cache_before=%s", workflowName, count, engine, train, cacheBefore)

artifacts = applyEvalsArtifact(artifacts, evalsOnly)

return DownloadWorkflowLogs(cmd.Context(), LogsDownloadOptions{
WorkflowName: workflowName,
Count: count,
Expand All @@ -343,6 +351,7 @@ Downloaded artifacts include (when using --artifacts all):
SummaryFile: summaryFile,
SafeOutputType: safeOutputType,
FilteredIntegrity: filteredIntegrity,
EvalsOnly: evalsOnly,
Train: train,
Format: format,
ReportFile: reportFile,
Expand All @@ -368,6 +377,7 @@ Downloaded artifacts include (when using --artifacts all):
logsCmd.Flags().Bool("no-firewall", false, "Filter to only runs without firewall enabled")
logsCmd.Flags().String("safe-output", "", "Filter to runs containing a specific safe output type (e.g., create-issue, missing-tool, missing-data, noop, report-incomplete)")
logsCmd.Flags().Bool("filtered-integrity", false, "Filter to runs containing items that were filtered by gateway integrity checks")
logsCmd.Flags().Bool("evals", false, "Filter to runs containing evals results (evals.jsonl); automatically includes the evals artifact")
logsCmd.Flags().Bool("parse", false, "Run JavaScript parsers on agent logs and firewall logs, writing Markdown to log.md and firewall.md")
addJSONFlag(logsCmd)
logsCmd.Flags().Int("timeout", 0, "Download timeout in minutes (0 = no timeout)")
Expand Down
4 changes: 3 additions & 1 deletion pkg/cli/logs_orchestrator.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,12 +168,13 @@ func DownloadWorkflowLogs(ctx context.Context, opts LogsDownloadOptions) error {
summaryFile := opts.SummaryFile
safeOutputType := opts.SafeOutputType
filteredIntegrity := opts.FilteredIntegrity
evalsOnly := opts.EvalsOnly
train := opts.Train
format := opts.Format
artifactSets := opts.ArtifactSets
after := opts.After

logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s, filteredIntegrity=%v, train=%v, format=%s, artifactSets=%v, after=%s", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType, filteredIntegrity, train, format, artifactSets, after)
logsOrchestratorLog.Printf("Starting workflow log download: workflow=%s, count=%d, startDate=%s, endDate=%s, outputDir=%s, summaryFile=%s, safeOutputType=%s, filteredIntegrity=%v, evalsOnly=%v, train=%v, format=%s, artifactSets=%v, after=%s", workflowName, count, startDate, endDate, outputDir, summaryFile, safeOutputType, filteredIntegrity, evalsOnly, train, format, artifactSets, after)

// Validate and resolve artifact sets into a concrete filter (list of artifact base names).
if err := ValidateArtifactSets(artifactSets); err != nil {
Expand Down Expand Up @@ -272,6 +273,7 @@ func DownloadWorkflowLogs(ctx context.Context, opts LogsDownloadOptions) error {
noFirewall: noFirewall,
safeOutputType: safeOutputType,
filteredIntegrity: filteredIntegrity,
evalsOnly: evalsOnly,
}

// Iterative algorithm: keep fetching runs until we have enough or exhaust available runs
Expand Down
12 changes: 12 additions & 0 deletions pkg/cli/logs_orchestrator_filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type runFilterOpts struct {
noFirewall bool
safeOutputType string
filteredIntegrity bool
evalsOnly bool
}

var fetchJobStatusesForProcessedRun = fetchJobStatuses
Expand Down Expand Up @@ -131,6 +132,17 @@ func applyRunFilters(result DownloadResult, opts runFilterOpts, verbose bool) bo
}
}

// Apply evals filtering if --evals flag is specified.
if opts.evalsOnly {
if !runHasEvals(result.LogsPath, verbose) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 04309d3downloadRunArtifactsConcurrent now bypasses the cached summary and falls through to a fresh download when the evals artifact is in the filter but not present locally, so the filter in applyRunFilters sees the just-downloaded artifact.

logsOrchestratorLog.Printf("Skipping run %d: no evals results found, filtered by --evals", result.Run.DatabaseID)
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Skipping run %d: workflow does not have evals results (filtered by --evals)", result.Run.DatabaseID)))
}
return true
}
}

return false
}

Expand Down
1 change: 1 addition & 0 deletions pkg/cli/logs_orchestrator_stdin.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ func DownloadWorkflowLogsFromStdin(ctx context.Context, opts StdinLogsOptions) e
noFirewall: opts.NoFirewall,
safeOutputType: opts.SafeOutputType,
filteredIntegrity: opts.FilteredIntegrity,
evalsOnly: opts.EvalsOnly,
}

// Process download results applying the same filters as DownloadWorkflowLogs.
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/logs_orchestrator_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type LogsDownloadOptions struct {
SummaryFile string
SafeOutputType string
FilteredIntegrity bool
EvalsOnly bool
Train bool
Format string
ArtifactSets []string
Expand All @@ -51,6 +52,7 @@ type StdinLogsOptions struct {
SummaryFile string
SafeOutputType string
FilteredIntegrity bool
EvalsOnly bool
Train bool
Format string
ReportFile string
Expand Down
Loading
Loading