From 3dfc0faac001550d6605c61edc7d1f27ea132d93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:34:14 +0000 Subject: [PATCH 1/9] Initial plan From 1dee27a8ce8caca3de15bfb4d6f878f598e0bbc2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:41:08 +0000 Subject: [PATCH 2/9] Add shared scanfindings package for scanner findings Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/scanfindings/scanfindings.go | 220 +++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) create mode 100644 pkg/scanfindings/scanfindings.go diff --git a/pkg/scanfindings/scanfindings.go b/pkg/scanfindings/scanfindings.go new file mode 100644 index 00000000000..9ce78df8621 --- /dev/null +++ b/pkg/scanfindings/scanfindings.go @@ -0,0 +1,220 @@ +// Package scanfindings provides a shared representation of the findings reported +// by the scanner integrations (zizmor, poutine, grype, grant, runner-guard, +// yamllint, ...). +// +// Each scanner speaks its own native JSON dialect, with severities spelled in a +// different vocabulary ("High", "error", "Negligible", "note", ...) and locations +// shaped differently. Integrations decode their native output into their own +// structs and then map those structs onto the shared Finding type declared here, +// so that severity classification, ordering and rendering are implemented once +// instead of once per tool. +package scanfindings + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/github/gh-aw/pkg/console" +) + +// SeverityLevel is the shared severity vocabulary used by every scanner +// integration. Native severity labels are normalized with ParseSeverity. +type SeverityLevel string + +const ( + // SeverityUnknown is used when a tool reports no severity, or one that + // cannot be mapped onto the shared vocabulary. + SeverityUnknown SeverityLevel = "unknown" + // SeverityInfo covers informational findings ("info", "note", "notice"). + SeverityInfo SeverityLevel = "info" + // SeverityLow covers low impact findings ("low", "negligible", "minor"). + SeverityLow SeverityLevel = "low" + // SeverityMedium covers medium impact findings ("medium", "moderate", "warning"). + SeverityMedium SeverityLevel = "medium" + // SeverityHigh covers high impact findings ("high", "error"). + SeverityHigh SeverityLevel = "high" + // SeverityCritical covers the most severe findings ("critical"). + SeverityCritical SeverityLevel = "critical" +) + +// ParseSeverity normalizes a native scanner severity label onto the shared +// vocabulary. Comparison is case-insensitive and unrecognized labels (including +// the empty string) map to SeverityUnknown. +func ParseSeverity(raw string) SeverityLevel { + switch strings.ToLower(strings.TrimSpace(raw)) { + case "critical", "crit": + return SeverityCritical + case "high", "error", "err": + return SeverityHigh + case "medium", "moderate", "warning", "warn": + return SeverityMedium + case "low", "negligible", "minor": + return SeverityLow + case "info", "information", "informational", "note", "notice": + return SeverityInfo + default: + return SeverityUnknown + } +} + +// String returns the canonical lowercase name of the severity. +func (s SeverityLevel) String() string { + return string(s) +} + +// Rank returns the relative ordering of a severity, with higher values meaning +// more severe. Unknown severities rank lowest. +func (s SeverityLevel) Rank() int { + switch s { + case SeverityCritical: + return 5 + case SeverityHigh: + return 4 + case SeverityMedium: + return 3 + case SeverityLow: + return 2 + case SeverityInfo: + return 1 + default: + return 0 + } +} + +// AtLeast reports whether the severity is at least as severe as min. +func (s SeverityLevel) AtLeast(min SeverityLevel) bool { + return s.Rank() >= min.Rank() +} + +// ErrorType maps the severity onto the console error type used when rendering a +// finding as a console.CompilerError. Unknown severities are rendered as +// warnings so that unclassified findings remain visible. +func (s SeverityLevel) ErrorType() string { + switch s { + case SeverityCritical, SeverityHigh: + return "error" + case SeverityLow, SeverityInfo: + return "info" + default: + return "warning" + } +} + +// Finding is the shared, tool-independent representation of a single scanner +// finding. Message holds the already-formatted, human readable description of +// the finding as produced by the owning integration. +type Finding struct { + RuleID string `json:"rule_id,omitempty"` + Severity SeverityLevel `json:"severity,omitempty"` + Message string `json:"message"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Column int `json:"column,omitempty"` + // Context holds the source lines surrounding the finding, used when + // rendering the finding to a terminal. It is optional. + Context []string `json:"-"` +} + +// CompilerError converts the finding into the console error format shared by all +// scanner output. Missing line and column values default to 1 so that the +// rendered position stays well formed. +func (f Finding) CompilerError() console.CompilerError { + line := f.Line + if line <= 0 { + line = 1 + } + column := f.Column + if column <= 0 { + column = 1 + } + + return console.CompilerError{ + Position: console.ErrorPosition{ + File: f.File, + Line: line, + Column: column, + }, + Type: f.Severity.ErrorType(), + Message: f.Message, + Context: f.Context, + } +} + +// FormatMessage builds the standard "[severity] rule: description" message used +// by the scanner integrations. Empty parts are omitted. +func FormatMessage(severityLabel, ruleID, description string) string { + var parts []string + if severityLabel != "" { + parts = append(parts, fmt.Sprintf("[%s]", severityLabel)) + } + if ruleID != "" { + if description != "" { + parts = append(parts, fmt.Sprintf("%s: %s", ruleID, description)) + } else { + parts = append(parts, ruleID) + } + } else if description != "" { + parts = append(parts, description) + } + return strings.Join(parts, " ") +} + +// Render writes the findings to w using the shared console error format. +func Render(w io.Writer, findings []Finding) { + for _, finding := range findings { + fmt.Fprint(w, console.FormatError(finding.CompilerError())) + } +} + +// Sort orders findings by file, then line, then column, then by decreasing +// severity, then by rule identifier. The ordering is stable and deterministic so +// that scanner output can be compared across runs. +func Sort(findings []Finding) { + sort.SliceStable(findings, func(i, j int) bool { + a, b := findings[i], findings[j] + if a.File != b.File { + return a.File < b.File + } + if a.Line != b.Line { + return a.Line < b.Line + } + if a.Column != b.Column { + return a.Column < b.Column + } + if a.Severity != b.Severity { + return a.Severity.Rank() > b.Severity.Rank() + } + return a.RuleID < b.RuleID + }) +} + +// CountAtLeast returns the number of findings with a severity of at least min. +func CountAtLeast(findings []Finding, min SeverityLevel) int { + count := 0 + for _, finding := range findings { + if finding.Severity.AtLeast(min) { + count++ + } + } + return count +} + +// ContextLines returns up to two source lines before and after the 1-based line +// number, used to display a finding in context. It returns nil when the line is +// out of range for the provided file lines. +func ContextLines(fileLines []string, line int) []string { + if len(fileLines) == 0 || line <= 0 || line > len(fileLines) { + return nil + } + + start := max(1, line-2) + end := min(len(fileLines), line+2) + + context := make([]string, 0, end-start+1) + for i := start; i <= end; i++ { + context = append(context, fileLines[i-1]) + } + return context +} From 00f0ceae989a45e327bf482a3c0aa9b1c6574742 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 02:55:18 +0000 Subject: [PATCH 3/9] Map all scanner integrations onto shared Finding/SeverityLevel type Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/audit_agent_output_test.go | 3 +- pkg/cli/audit_agentic_analysis.go | 3 +- pkg/cli/audit_report.go | 11 +- pkg/cli/audit_report_render.go | 5 +- pkg/cli/audit_report_test.go | 13 +- pkg/cli/grant.go | 36 ++-- pkg/cli/grype.go | 53 +++--- pkg/cli/poutine.go | 181 ++++++------------ pkg/cli/runner_guard.go | 72 +++---- pkg/cli/validation_issue.go | 18 ++ pkg/cli/yamllint.go | 56 ++---- pkg/cli/yamllint_test.go | 29 +-- pkg/cli/zizmor.go | 92 ++++----- pkg/scanfindings/README.md | 64 +++++++ pkg/scanfindings/scanfindings.go | 24 +-- pkg/scanfindings/scanfindings_test.go | 220 ++++++++++++++++++++++ pkg/workflow/markdown_security_scanner.go | 32 +++- 17 files changed, 556 insertions(+), 356 deletions(-) create mode 100644 pkg/scanfindings/README.md create mode 100644 pkg/scanfindings/scanfindings_test.go diff --git a/pkg/cli/audit_agent_output_test.go b/pkg/cli/audit_agent_output_test.go index 0d69bd3ce54..0376a3cec8d 100644 --- a/pkg/cli/audit_agent_output_test.go +++ b/pkg/cli/audit_agent_output_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/workflow" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -126,7 +127,7 @@ func TestKeyFindingsGeneration(t *testing.T) { } require.NotNil(t, failureFinding, "Expected an error finding with 'Failed' in title for scenario %q", tt.name) - assert.Equal(t, "critical", failureFinding.Severity, + assert.Equal(t, scanfindings.SeverityCritical, failureFinding.Severity, "Expected critical severity for failure finding in scenario %q", tt.name) } diff --git a/pkg/cli/audit_agentic_analysis.go b/pkg/cli/audit_agentic_analysis.go index 55b5f7b8cb3..35cd57a875b 100644 --- a/pkg/cli/audit_agentic_analysis.go +++ b/pkg/cli/audit_agentic_analysis.go @@ -8,6 +8,7 @@ import ( "time" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/timeutil" "github.com/github/gh-aw/pkg/workflow" ) @@ -431,7 +432,7 @@ func generateAgenticAssessmentFindings(assessments []AgenticAssessment) []Findin } findings = append(findings, Finding{ Category: category, - Severity: assessment.Severity, + Severity: scanfindings.ParseSeverity(assessment.Severity), Title: prettifyAssessmentKind(assessment.Kind), Description: assessment.Summary, Impact: impact, diff --git a/pkg/cli/audit_report.go b/pkg/cli/audit_report.go index fa5bab7d2dd..ffdecc5b3b1 100644 --- a/pkg/cli/audit_report.go +++ b/pkg/cli/audit_report.go @@ -16,6 +16,7 @@ import ( "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/github" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/sliceutil" "github.com/github/gh-aw/pkg/stringutil" "github.com/github/gh-aw/pkg/timeutil" @@ -64,11 +65,11 @@ type AuditData struct { // Finding represents a key insight discovered during audit type Finding struct { - Category string `json:"category"` // e.g., "error", "performance", "cost", "tooling" - Severity string `json:"severity"` // "critical", "high", "medium", "low", "info" - Title string `json:"title"` // Brief title - Description string `json:"description"` // Detailed description - Impact string `json:"impact,omitempty"` // What impact this has + Category string `json:"category"` // e.g., "error", "performance", "cost", "tooling" + Severity scanfindings.SeverityLevel `json:"severity"` // shared severity vocabulary + Title string `json:"title"` // Brief title + Description string `json:"description"` // Detailed description + Impact string `json:"impact,omitempty"` // What impact this has } // Recommendation represents an actionable suggestion diff --git a/pkg/cli/audit_report_render.go b/pkg/cli/audit_report_render.go index 4451f356d6a..56bf614e771 100644 --- a/pkg/cli/audit_report_render.go +++ b/pkg/cli/audit_report_render.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/github/gh-aw/pkg/console" + "github.com/github/gh-aw/pkg/scanfindings" ) // renderJSON outputs the audit data as JSON @@ -237,7 +238,7 @@ func renderConsoleFindings(findings []Finding) { } fmt.Fprintln(os.Stderr, " findings:") for _, finding := range findings { - fmt.Fprintf(os.Stderr, " [%s] %s: %s\n", strings.ToUpper(finding.Severity), finding.Title, finding.Description) + fmt.Fprintf(os.Stderr, " [%s] %s: %s\n", strings.ToUpper(finding.Severity.String()), finding.Title, finding.Description) } } @@ -440,7 +441,7 @@ func renderConsoleLogsPath(logsPath string) { func filterActionableFindings(findings []Finding) []Finding { var result []Finding for _, f := range findings { - if f.Severity == "critical" || f.Severity == "high" || f.Severity == "medium" || f.Severity == "low" { + if f.Severity.AtLeast(scanfindings.SeverityLow) { result = append(result, f) } } diff --git a/pkg/cli/audit_report_test.go b/pkg/cli/audit_report_test.go index 93bbfc5dce2..0928eefb720 100644 --- a/pkg/cli/audit_report_test.go +++ b/pkg/cli/audit_report_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/testutil" "github.com/github/gh-aw/pkg/workflow" "github.com/stretchr/testify/assert" @@ -55,7 +56,7 @@ func createTestProcessedRun(opts ...func(*ProcessedRun)) ProcessedRun { func assertFindingExists(t *testing.T, findings []Finding, category, severity string, msgAndArgs ...any) { t.Helper() for _, f := range findings { - if f.Category == category && f.Severity == severity { + if f.Category == category && f.Severity.String() == severity { return // Found it! } } @@ -142,7 +143,7 @@ func TestGenerateFindings(t *testing.T) { checkFindings: func(t *testing.T, findings []Finding) { finding := findFindingByCategory(findings, "error") require.NotNil(t, finding, "Failed workflow should generate an error finding") - assert.Equal(t, "critical", finding.Severity, "Error finding should have critical severity") + assert.Equal(t, scanfindings.SeverityCritical, finding.Severity, "Error finding should have critical severity") assert.Contains(t, finding.Title, "Failed", "Error finding should have 'Failed' in title") assert.Contains(t, finding.Description, "Test error", "Error finding description should include the first error message") }, @@ -179,7 +180,7 @@ func TestGenerateFindings(t *testing.T) { checkFindings: func(t *testing.T, findings []Finding) { finding := findFindingByCategory(findings, "error") require.NotNil(t, finding, "Failed workflow should generate an error finding") - assert.Equal(t, "critical", finding.Severity, "Error finding should have critical severity") + assert.Equal(t, scanfindings.SeverityCritical, finding.Severity, "Error finding should have critical severity") assert.Equal(t, "Workflow 'Test Workflow' failed with 1 error(s)", finding.Description, "Description should match standard format without error message suffix when no errors available") }, @@ -202,7 +203,7 @@ func TestGenerateFindings(t *testing.T) { checkFindings: func(t *testing.T, findings []Finding) { finding := findFindingByCategory(findings, "error") require.NotNil(t, finding, "Failed workflow should generate an error finding") - assert.Equal(t, "critical", finding.Severity, "Error finding should have critical severity") + assert.Equal(t, scanfindings.SeverityCritical, finding.Severity, "Error finding should have critical severity") assert.Contains(t, finding.Description, "2 error(s)", "Description should reflect the actual number of errors, not metrics.ErrorCount") assert.NotContains(t, finding.Description, "0 error(s)", @@ -224,7 +225,7 @@ func TestGenerateFindings(t *testing.T) { checkFindings: func(t *testing.T, findings []Finding) { finding := findFindingByCategory(findings, "error") require.NotNil(t, finding, "Failed workflow should still generate an error finding") - assert.Equal(t, "critical", finding.Severity, "Error finding should have critical severity") + assert.Equal(t, scanfindings.SeverityCritical, finding.Severity, "Error finding should have critical severity") assert.Contains(t, finding.Description, "before agent activation", "Description should indicate pre-activation failure when no logs are available") assert.Contains(t, finding.Description, "no error logs", @@ -1162,7 +1163,7 @@ func TestFindingSeverityOrdering(t *testing.T) { // Should have critical, high, and medium findings severityCounts := make(map[string]int) for _, f := range findings { - severityCounts[f.Severity]++ + severityCounts[f.Severity.String()]++ } assert.NotZero(t, severityCounts["critical"], diff --git a/pkg/cli/grant.go b/pkg/cli/grant.go index 3fc26c0c789..fc5a844fab3 100644 --- a/pkg/cli/grant.go +++ b/pkg/cli/grant.go @@ -15,6 +15,7 @@ import ( "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/workflow" "gopkg.in/yaml.v3" ) @@ -330,7 +331,17 @@ func grantDisplayFindings(imageTag string, output *grantOutput) (int, error) { return 0, nil } - total := 0 + findings := grantFindingsToShared(imageTag, output) + scanfindings.Render(os.Stderr, findings) + + return len(findings), nil +} + +// grantFindingsToShared maps grant's denied license packages onto the shared +// finding representation used by every scanner integration. Container images have +// no source location, so the image tag is reported as the finding location. +func grantFindingsToShared(imageTag string, output *grantOutput) []scanfindings.Finding { + var findings []scanfindings.Finding for _, target := range output.Run.Targets { for _, pkg := range target.Evaluation.Findings.Packages { if pkg.Decision != "deny" { @@ -354,22 +365,17 @@ func grantDisplayFindings(imageTag string, output *grantOutput) (int, error) { } } - message := fmt.Sprintf("license policy violation: %s (%s)", grantPackageRef(pkg), licenses) - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: imageTag, - Line: 1, - Column: 1, - }, - Type: "error", - Message: message, - } - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) - total++ + findings = append(findings, scanfindings.Finding{ + RuleID: "license-policy", + Severity: scanfindings.SeverityHigh, + Message: fmt.Sprintf("license policy violation: %s (%s)", grantPackageRef(pkg), licenses), + File: imageTag, + Line: 1, + Column: 1, + }) } } - - return total, nil + return findings } func grantPackageRef(pkg grantPackageFinding) string { diff --git a/pkg/cli/grype.go b/pkg/cli/grype.go index 4098238fbf4..03e1b9f5a3d 100644 --- a/pkg/cli/grype.go +++ b/pkg/cli/grype.go @@ -36,6 +36,7 @@ import ( "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/workflow" ) @@ -395,26 +396,28 @@ func grypeDisplayFindings(imageTag string, output *grypeOutput) int { return 0 } - for _, match := range output.Matches { + findings := grypeFindingsToShared(imageTag, output.Matches) + scanfindings.Render(os.Stderr, findings) + + return len(findings) +} + +// grypeFindingsToShared maps grype's native vulnerability matches onto the shared +// finding representation used by every scanner integration. Container images have +// no source location, so the image tag is reported as the finding location. +func grypeFindingsToShared(imageTag string, matches []grypeFinding) []scanfindings.Finding { + findings := make([]scanfindings.Finding, 0, len(matches)) + for _, match := range matches { vuln := match.Vulnerability art := match.Artifact - severity := vuln.Severity - if severity == "" { - severity = "Unknown" - } - - // Map severity to error type for display purposes. - errorType := "warning" - switch strings.ToLower(severity) { - case "critical", "high": - errorType = "error" - case "low", "negligible", "informational": - errorType = "info" + severityLabel := vuln.Severity + if severityLabel == "" { + severityLabel = "Unknown" } // Build a compact message: [Severity] CVE-ID: package@version (fix: x.y.z) (url) - message := fmt.Sprintf("[%s] %s: %s@%s", severity, vuln.ID, art.Name, art.Version) + message := fmt.Sprintf("[%s] %s: %s@%s", severityLabel, vuln.ID, art.Name, art.Version) if len(vuln.Fix.Versions) > 0 { message = fmt.Sprintf("%s (fix: %s)", message, strings.Join(vuln.Fix.Versions, ", ")) } @@ -422,18 +425,14 @@ func grypeDisplayFindings(imageTag string, output *grypeOutput) int { message = fmt.Sprintf("%s (%s)", message, vuln.DataSource) } - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: imageTag, - Line: 1, - Column: 1, - }, - Type: errorType, - Message: message, - } - - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) + findings = append(findings, scanfindings.Finding{ + RuleID: vuln.ID, + Severity: scanfindings.ParseSeverity(severityLabel), + Message: message, + File: imageTag, + Line: 1, + Column: 1, + }) } - - return len(output.Matches) + return findings } diff --git a/pkg/cli/poutine.go b/pkg/cli/poutine.go index f85686a0ca4..8658ac10a52 100644 --- a/pkg/cli/poutine.go +++ b/pkg/cli/poutine.go @@ -16,6 +16,7 @@ import ( "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" ) var poutineLog = logger.New("cli:poutine") @@ -31,15 +32,21 @@ type poutineFinding struct { } `json:"meta"` } +// poutineRule describes a poutine rule definition from the JSON output +type poutineRule struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Level string `json:"level"` // error, warning, note +} + +// poutineRules maps rule identifiers to their definitions +type poutineRules map[string]poutineRule + // poutineOutput represents the complete JSON output from poutine type poutineOutput struct { Findings []poutineFinding `json:"findings"` - Rules map[string]struct { - ID string `json:"id"` - Title string `json:"title"` - Description string `json:"description"` - Level string `json:"level"` // error, warning, note - } `json:"rules"` + Rules poutineRules `json:"rules"` } // ensurePoutineConfig creates .poutine.yml to configure allowed runners and @@ -372,68 +379,8 @@ func parseAndDisplayPoutineOutput(stdout, targetFile string, verbose bool) (int, fileLines = strings.Split(string(fileContent), "\n") } - // Display detailed findings using CompilerError format - for _, finding := range relevantFindings { - // Get rule details - ruleInfo := output.Rules[finding.RuleID] - severity := ruleInfo.Level - if severity == "" { - severity = "warning" // Default to warning if not specified - } - - title := ruleInfo.Title - if title == "" { - title = finding.RuleID - } - - // Get line number (poutine uses 1-based indexing) - lineNum := finding.Meta.Line - if lineNum == 0 { - lineNum = 1 // Default to line 1 if not specified - } - - // Create context lines around the error - var context []string - if len(fileLines) > 0 && lineNum > 0 && lineNum <= len(fileLines) { - startLine := max(1, lineNum-2) - endLine := min(len(fileLines), lineNum+2) - - for i := startLine; i <= endLine; i++ { - if i-1 < len(fileLines) { - context = append(context, fileLines[i-1]) - } - } - } - - // Map severity to error type - errorType := "warning" - switch severity { - case "error": - errorType = "error" - case "note": - errorType = "info" - } - - // Build message with details - message := fmt.Sprintf("[%s] %s: %s", severity, finding.RuleID, title) - if finding.Meta.Details != "" { - message = fmt.Sprintf("%s - %s", message, finding.Meta.Details) - } - - // Create and format CompilerError - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: finding.Meta.Path, - Line: lineNum, - Column: 1, // poutine doesn't provide column info - }, - Type: errorType, - Message: message, - Context: context, - } - - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) - } + // Display detailed findings using the shared finding representation + scanfindings.Render(os.Stderr, poutineFindingsToShared(relevantFindings, output.Rules, targetFile, fileLines)) return totalWarnings, nil } @@ -515,69 +462,51 @@ func parseAndDisplayPoutineOutputForDirectory(stdout string, verbose bool, gitRo fileLines = strings.Split(string(fileContent), "\n") } - // Display detailed findings using CompilerError format - for _, finding := range findings { - // Get rule details - ruleInfo := output.Rules[finding.RuleID] - severity := ruleInfo.Level - if severity == "" { - severity = "warning" // Default to warning if not specified - } - - title := ruleInfo.Title - if title == "" { - title = finding.RuleID - } - - // Get line number (poutine uses 1-based indexing) - lineNum := finding.Meta.Line - if lineNum == 0 { - lineNum = 1 // Default to line 1 if not specified - } - - // Create context lines around the error - var context []string - if len(fileLines) > 0 && lineNum > 0 && lineNum <= len(fileLines) { - startLine := max(1, lineNum-2) - endLine := min(len(fileLines), lineNum+2) + // Display detailed findings using the shared finding representation + scanfindings.Render(os.Stderr, poutineFindingsToShared(findings, output.Rules, filePath, fileLines)) + } - for i := startLine; i <= endLine; i++ { - if i-1 < len(fileLines) { - context = append(context, fileLines[i-1]) - } - } - } + return totalWarnings, nil +} - // Map severity to error type - errorType := "warning" - switch severity { - case "error": - errorType = "error" - case "note": - errorType = "info" - } +// poutineFindingsToShared maps poutine's native findings onto the shared finding +// representation used by every scanner integration. Rule metadata supplies the +// severity level and title when available. +func poutineFindingsToShared(findings []poutineFinding, rules poutineRules, filePath string, fileLines []string) []scanfindings.Finding { + shared := make([]scanfindings.Finding, 0, len(findings)) + for _, finding := range findings { + ruleInfo := rules[finding.RuleID] + + severityLabel := ruleInfo.Level + if severityLabel == "" { + severityLabel = "warning" // Default to warning if not specified + } - // Build message with details - message := fmt.Sprintf("[%s] %s: %s", severity, finding.RuleID, title) - if finding.Meta.Details != "" { - message = fmt.Sprintf("%s - %s", message, finding.Meta.Details) - } + title := ruleInfo.Title + if title == "" { + title = finding.RuleID + } - // Create and format CompilerError - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: finding.Meta.Path, - Line: lineNum, - Column: 1, // poutine doesn't provide column info - }, - Type: errorType, - Message: message, - Context: context, - } + // Get line number (poutine uses 1-based indexing) + lineNum := finding.Meta.Line + if lineNum == 0 { + lineNum = 1 // Default to line 1 if not specified + } - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) + message := scanfindings.FormatMessage(severityLabel, finding.RuleID, title) + if finding.Meta.Details != "" { + message = fmt.Sprintf("%s - %s", message, finding.Meta.Details) } - } - return totalWarnings, nil + shared = append(shared, scanfindings.Finding{ + RuleID: finding.RuleID, + Severity: scanfindings.ParseSeverity(severityLabel), + Message: message, + File: filePath, + Line: lineNum, + Column: 1, // poutine doesn't provide column info + Context: scanfindings.ContextLines(fileLines, lineNum), + }) + } + return shared } diff --git a/pkg/cli/runner_guard.go b/pkg/cli/runner_guard.go index 4a8ecf3ba57..d290a64bc55 100644 --- a/pkg/cli/runner_guard.go +++ b/pkg/cli/runner_guard.go @@ -14,6 +14,7 @@ import ( "github.com/github/gh-aw/pkg/fileutil" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" ) var runnerGuardLog = logger.New("cli:runner_guard") @@ -299,53 +300,36 @@ func parseAndDisplayRunnerGuardOutput(stdout string, verbose bool, gitRoot strin fileLines = strings.Split(string(fileContent), "\n") } - for _, finding := range findings { - lineNum := finding.Line - if lineNum == 0 { - lineNum = 1 - } - - // Create context lines around the finding - var context []string - if len(fileLines) > 0 && lineNum > 0 && lineNum <= len(fileLines) { - startLine := max(1, lineNum-2) - endLine := min(len(fileLines), lineNum+2) - for i := startLine; i <= endLine; i++ { - if i-1 < len(fileLines) { - context = append(context, fileLines[i-1]) - } - } - } - - // Map severity to error type - errorType := "warning" - switch strings.ToLower(finding.Severity) { - case "critical", "high", "error": - errorType = "error" - case "note", "info": - errorType = "info" - } + scanfindings.Render(os.Stderr, runnerGuardFindingsToShared(findings, fileLines)) + } - // Build message - message := fmt.Sprintf("[%s] %s: %s", finding.Severity, finding.RuleID, finding.Name) - if finding.Description != "" { - message = fmt.Sprintf("%s - %s", message, finding.Description) - } + return totalFindings, nil +} - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: finding.File, - Line: lineNum, - Column: 1, - }, - Type: errorType, - Message: message, - Context: context, - } +// runnerGuardFindingsToShared maps runner-guard's native findings onto the shared +// finding representation used by every scanner integration. +func runnerGuardFindingsToShared(findings []runnerGuardFinding, fileLines []string) []scanfindings.Finding { + shared := make([]scanfindings.Finding, 0, len(findings)) + for _, finding := range findings { + lineNum := finding.Line + if lineNum == 0 { + lineNum = 1 + } - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) + message := scanfindings.FormatMessage(finding.Severity, finding.RuleID, finding.Name) + if finding.Description != "" { + message = fmt.Sprintf("%s - %s", message, finding.Description) } - } - return totalFindings, nil + shared = append(shared, scanfindings.Finding{ + RuleID: finding.RuleID, + Severity: scanfindings.ParseSeverity(finding.Severity), + Message: message, + File: finding.File, + Line: lineNum, + Column: 1, + Context: scanfindings.ContextLines(fileLines, lineNum), + }) + } + return shared } diff --git a/pkg/cli/validation_issue.go b/pkg/cli/validation_issue.go index 795f75220e5..bbeb9cf07ef 100644 --- a/pkg/cli/validation_issue.go +++ b/pkg/cli/validation_issue.go @@ -1,5 +1,7 @@ package cli +import "github.com/github/gh-aw/pkg/scanfindings" + // ValidationIssue represents a single validation, warning, or audit issue entry. type ValidationIssue struct { Type string `json:"type"` @@ -7,3 +9,19 @@ type ValidationIssue struct { Line int `json:"line,omitempty"` File string `json:"file,omitempty"` } + +// Severity maps the issue type ("error", "warning", ...) onto the shared +// severity vocabulary used by the scanner integrations. +func (v ValidationIssue) Severity() scanfindings.SeverityLevel { + return scanfindings.ParseSeverity(v.Type) +} + +// ToFinding converts the validation issue to the shared finding representation. +func (v ValidationIssue) ToFinding() scanfindings.Finding { + return scanfindings.Finding{ + Severity: v.Severity(), + Message: v.Message, + File: v.File, + Line: v.Line, + } +} diff --git a/pkg/cli/yamllint.go b/pkg/cli/yamllint.go index e9ea09df3d8..98dcf1fe509 100644 --- a/pkg/cli/yamllint.go +++ b/pkg/cli/yamllint.go @@ -15,6 +15,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" ) var yamllintLog = logger.New("cli:yamllint") @@ -23,16 +24,6 @@ var yamllintLog = logger.New("cli:yamllint") // It disables rules that produce excessive noise on generated YAML output. const yamllintDefaultConfig = `{extends: default, rules: {line-length: disable, document-start: disable, truthy: {check-keys: false}, comments: {require-starting-space: true, min-spaces-from-content: 1}}}` -// yamllintIssue represents a single issue from yamllint parsable output. -type yamllintIssue struct { - File string - Line int - Column int - Level string - Message string - Rule string -} - // yamllintParsableRegex matches a single line of yamllint --format parsable output: // // {file}:{line}:{col}: [{level}] {message} ({rule}) @@ -202,22 +193,7 @@ func parseAndDisplayYamllintOutput(stdout string) (int, error) { totalIssues++ - errorType := "warning" - if issue.Level == "error" { - errorType = "error" - } - - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: issue.File, - Line: issue.Line, - Column: issue.Column, - }, - Type: errorType, - Message: fmt.Sprintf("[%s] %s (%s)", issue.Level, issue.Message, issue.Rule), - } - - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) + scanfindings.Render(os.Stderr, []scanfindings.Finding{issue}) } if err := scanner.Err(); err != nil { @@ -227,30 +203,34 @@ func parseAndDisplayYamllintOutput(stdout string) (int, error) { return totalIssues, nil } -// parseYamllintLine parses a single line of yamllint --format parsable output. +// parseYamllintLine parses a single line of yamllint --format parsable output +// into the shared finding representation. // Expected format: {file}:{line}:{col}: [{level}] {message} ({rule}) -func parseYamllintLine(line string) (yamllintIssue, error) { +func parseYamllintLine(line string) (scanfindings.Finding, error) { matches := yamllintParsableRegex.FindStringSubmatch(line) if matches == nil { - return yamllintIssue{}, fmt.Errorf("line does not match yamllint parsable format: %q", line) + return scanfindings.Finding{}, fmt.Errorf("line does not match yamllint parsable format: %q", line) } lineNum, err := strconv.Atoi(matches[2]) if err != nil { - return yamllintIssue{}, fmt.Errorf("failed to parse line number %q: %w", matches[2], err) + return scanfindings.Finding{}, fmt.Errorf("failed to parse line number %q: %w", matches[2], err) } colNum, err := strconv.Atoi(matches[3]) if err != nil { - return yamllintIssue{}, fmt.Errorf("failed to parse column number %q: %w", matches[3], err) + return scanfindings.Finding{}, fmt.Errorf("failed to parse column number %q: %w", matches[3], err) } - return yamllintIssue{ - File: matches[1], - Line: lineNum, - Column: colNum, - Level: matches[4], - Message: matches[5], - Rule: matches[6], + level := matches[4] + rule := matches[6] + + return scanfindings.Finding{ + RuleID: rule, + Severity: scanfindings.ParseSeverity(level), + Message: fmt.Sprintf("[%s] %s (%s)", level, matches[5], rule), + File: matches[1], + Line: lineNum, + Column: colNum, }, nil } diff --git a/pkg/cli/yamllint_test.go b/pkg/cli/yamllint_test.go index 8cc20978923..728737bf767 100644 --- a/pkg/cli/yamllint_test.go +++ b/pkg/cli/yamllint_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -17,13 +18,13 @@ func TestParseYamllintLine(t *testing.T) { issue, err := parseYamllintLine("./.github/workflows/test.lock.yml:7:9: [error] wrong indentation: expected 8 but found 10 (indentation)") require.NoError(t, err) - assert.Equal(t, yamllintIssue{ - File: "./.github/workflows/test.lock.yml", - Line: 7, - Column: 9, - Level: "error", - Message: "wrong indentation: expected 8 but found 10", - Rule: "indentation", + assert.Equal(t, scanfindings.Finding{ + RuleID: "indentation", + Severity: scanfindings.SeverityHigh, + Message: "[error] wrong indentation: expected 8 but found 10 (indentation)", + File: "./.github/workflows/test.lock.yml", + Line: 7, + Column: 9, }, issue) }) @@ -31,13 +32,13 @@ func TestParseYamllintLine(t *testing.T) { issue, err := parseYamllintLine("./test.lock.yml:1:1: [warning] missing document start \"---\" (document-start)") require.NoError(t, err) - assert.Equal(t, yamllintIssue{ - File: "./test.lock.yml", - Line: 1, - Column: 1, - Level: "warning", - Message: "missing document start \"---\"", - Rule: "document-start", + assert.Equal(t, scanfindings.Finding{ + RuleID: "document-start", + Severity: scanfindings.SeverityMedium, + Message: "[warning] missing document start \"---\" (document-start)", + File: "./test.lock.yml", + Line: 1, + Column: 1, }, issue) }) diff --git a/pkg/cli/zizmor.go b/pkg/cli/zizmor.go index d62302b5e11..15a2f002985 100644 --- a/pkg/cli/zizmor.go +++ b/pkg/cli/zizmor.go @@ -15,6 +15,7 @@ import ( "github.com/github/gh-aw/pkg/console" "github.com/github/gh-aw/pkg/gitutil" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/setutil" ) @@ -222,7 +223,7 @@ func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) (int, int, }{} fileFindings[filePath] = append(fileFindings[filePath], finding) totalWarnings++ - if finding.Determinations.Severity == "High" || finding.Determinations.Severity == "Critical" { + if scanfindings.ParseSeverity(finding.Determinations.Severity).AtLeast(scanfindings.SeverityHigh) { highSeverityCount++ } } @@ -269,63 +270,42 @@ func parseAndDisplayZizmorOutput(stdout, stderr string, verbose bool) (int, int, fileLines = strings.Split(string(fileContent), "\n") } - // Display detailed findings using CompilerError format - for _, finding := range findings { - severity := finding.Determinations.Severity - ident := finding.Ident - desc := finding.Desc - url := finding.URL - - // Find the primary location (first location in the list) - if len(finding.Locations) > 0 { - loc := finding.Locations[0] - row := loc.Concrete.Location.StartPoint.Row - col := loc.Concrete.Location.StartPoint.Column - // Zizmor uses 0-based indexing, convert to 1-based for user display - lineNum := row + 1 - colNum := col + 1 - - // Create context lines around the error - var context []string - if len(fileLines) > 0 && lineNum > 0 && lineNum <= len(fileLines) { - startLine := max(1, lineNum-2) - endLine := min(len(fileLines), lineNum+2) - - for i := startLine; i <= endLine; i++ { - if i-1 < len(fileLines) { - context = append(context, fileLines[i-1]) - } - } - } - - // Map severity to error type - errorType := "warning" - if severity == "High" || severity == "Critical" { - errorType = "error" - } - - // Build message with URL link if available - message := fmt.Sprintf("[%s] %s: %s", severity, ident, desc) - if url != "" { - message = fmt.Sprintf("%s (%s)", message, url) - } + // Display detailed findings using the shared finding representation + scanfindings.Render(os.Stderr, zizmorFindingsToShared(filePath, findings, fileLines)) + } - // Create and format CompilerError - compilerErr := console.CompilerError{ - Position: console.ErrorPosition{ - File: filePath, - Line: lineNum, - Column: colNum, - }, - Type: errorType, - Message: message, - Context: context, - } + return totalWarnings, highSeverityCount, nil +} - fmt.Fprint(os.Stderr, console.FormatError(compilerErr)) - } +// zizmorFindingsToShared maps zizmor's native findings onto the shared finding +// representation used by every scanner integration. +func zizmorFindingsToShared(filePath string, findings []zizmorFinding, fileLines []string) []scanfindings.Finding { + shared := make([]scanfindings.Finding, 0, len(findings)) + for _, finding := range findings { + // Use the primary location (first location in the list) + if len(finding.Locations) == 0 { + continue + } + loc := finding.Locations[0] + // Zizmor uses 0-based indexing, convert to 1-based for user display + lineNum := loc.Concrete.Location.StartPoint.Row + 1 + colNum := loc.Concrete.Location.StartPoint.Column + 1 + + // Build message with URL link if available + message := scanfindings.FormatMessage(finding.Determinations.Severity, finding.Ident, finding.Desc) + if finding.URL != "" { + message = fmt.Sprintf("%s (%s)", message, finding.URL) } - } - return totalWarnings, highSeverityCount, nil + shared = append(shared, scanfindings.Finding{ + RuleID: finding.Ident, + Severity: scanfindings.ParseSeverity(finding.Determinations.Severity), + Message: message, + File: filePath, + Line: lineNum, + Column: colNum, + Context: scanfindings.ContextLines(fileLines, lineNum), + }) + } + return shared } diff --git a/pkg/scanfindings/README.md b/pkg/scanfindings/README.md new file mode 100644 index 00000000000..dfe785abd4b --- /dev/null +++ b/pkg/scanfindings/README.md @@ -0,0 +1,64 @@ +# scanfindings Package + +The `scanfindings` package provides a shared representation of the findings reported by the scanner integrations (zizmor, poutine, grype, grant, runner-guard, yamllint, markdown security scan, audit findings). + +## Overview + +Each scanner speaks its own native JSON dialect, with severities spelled in a different vocabulary (`High`, `error`, `Negligible`, `note`, ...) and locations shaped differently. Integrations decode their native output into their own structs and then map those structs onto the shared `Finding` type declared here, so severity classification, ordering and rendering are implemented once instead of once per tool. + +## Public API + +### Types + +| Type | Description | +|------|-------------| +| `SeverityLevel` | Shared severity vocabulary: `unknown`, `info`, `low`, `medium`, `high`, `critical` | +| `Finding` | Tool-independent finding: `RuleID`, `Severity`, `Message`, `File`, `Line`, `Column`, `Context` | + +### Functions and methods + +| Function | Signature | Description | +|----------|-----------|-------------| +| `ParseSeverity` | `func ParseSeverity(raw string) SeverityLevel` | Normalizes a native severity label (case-insensitive) | +| `SeverityLevel.String` | `func (s SeverityLevel) String() string` | Canonical lowercase severity name | +| `SeverityLevel.Rank` | `func (s SeverityLevel) Rank() int` | Relative ordering, higher is more severe | +| `SeverityLevel.AtLeast` | `func (s SeverityLevel) AtLeast(min SeverityLevel) bool` | Severity threshold comparison | +| `SeverityLevel.ErrorType` | `func (s SeverityLevel) ErrorType() string` | Console error type (`error`, `warning`, `info`) | +| `Finding.CompilerError` | `func (f Finding) CompilerError() console.CompilerError` | Converts a finding to the console error format | +| `FormatMessage` | `func FormatMessage(severityLabel, ruleID, description string) string` | Builds the `[severity] rule: description` message | +| `Render` | `func Render(w io.Writer, findings []Finding)` | Writes findings using the shared console format | +| `Sort` | `func Sort(findings []Finding)` | Orders findings by file, line, column, severity, rule | +| `CountAtLeast` | `func CountAtLeast(findings []Finding, min SeverityLevel) int` | Counts findings at or above a severity | +| `ContextLines` | `func ContextLines(fileLines []string, line int) []string` | Returns the source lines surrounding a finding | + +## Usage Examples + +```go +import "github.com/github/gh-aw/pkg/scanfindings" + +findings := []scanfindings.Finding{{ + RuleID: "template-injection", + Severity: scanfindings.ParseSeverity("High"), + Message: scanfindings.FormatMessage("High", "template-injection", "template injection with untrusted input"), + File: ".github/workflows/demo.lock.yml", + Line: 12, + Column: 24, +}} + +scanfindings.Sort(findings) +scanfindings.Render(os.Stderr, findings) + +highCount := scanfindings.CountAtLeast(findings, scanfindings.SeverityHigh) +``` + +## Dependencies + +**Internal**: +- `pkg/console` — console error formatting + +**External**: +- None beyond the Go standard library. + +--- + +*This specification is automatically maintained by the [spec-extractor](../../.github/workflows/spec-extractor.md) workflow.* diff --git a/pkg/scanfindings/scanfindings.go b/pkg/scanfindings/scanfindings.go index 9ce78df8621..d8b161134f7 100644 --- a/pkg/scanfindings/scanfindings.go +++ b/pkg/scanfindings/scanfindings.go @@ -11,9 +11,10 @@ package scanfindings import ( + "cmp" "fmt" "io" - "sort" + "slices" "strings" "github.com/github/gh-aw/pkg/console" @@ -172,21 +173,20 @@ func Render(w io.Writer, findings []Finding) { // severity, then by rule identifier. The ordering is stable and deterministic so // that scanner output can be compared across runs. func Sort(findings []Finding) { - sort.SliceStable(findings, func(i, j int) bool { - a, b := findings[i], findings[j] - if a.File != b.File { - return a.File < b.File + slices.SortStableFunc(findings, func(a, b Finding) int { + if c := strings.Compare(a.File, b.File); c != 0 { + return c } - if a.Line != b.Line { - return a.Line < b.Line + if c := cmp.Compare(a.Line, b.Line); c != 0 { + return c } - if a.Column != b.Column { - return a.Column < b.Column + if c := cmp.Compare(a.Column, b.Column); c != 0 { + return c } - if a.Severity != b.Severity { - return a.Severity.Rank() > b.Severity.Rank() + if c := cmp.Compare(b.Severity.Rank(), a.Severity.Rank()); c != 0 { + return c } - return a.RuleID < b.RuleID + return strings.Compare(a.RuleID, b.RuleID) }) } diff --git a/pkg/scanfindings/scanfindings_test.go b/pkg/scanfindings/scanfindings_test.go new file mode 100644 index 00000000000..754ec55a5cc --- /dev/null +++ b/pkg/scanfindings/scanfindings_test.go @@ -0,0 +1,220 @@ +package scanfindings + +import ( + "bytes" + "strings" + "testing" +) + +func TestParseSeverity(t *testing.T) { + tests := []struct { + raw string + want SeverityLevel + }{ + {"Critical", SeverityCritical}, + {"critical", SeverityCritical}, + {"High", SeverityHigh}, + {"error", SeverityHigh}, + {"Medium", SeverityMedium}, + {"warning", SeverityMedium}, + {"moderate", SeverityMedium}, + {"Low", SeverityLow}, + {"Negligible", SeverityLow}, + {"Informational", SeverityInfo}, + {"note", SeverityInfo}, + {"info", SeverityInfo}, + {" High ", SeverityHigh}, + {"Unknown", SeverityUnknown}, + {"", SeverityUnknown}, + {"bogus", SeverityUnknown}, + } + + for _, tt := range tests { + if got := ParseSeverity(tt.raw); got != tt.want { + t.Errorf("ParseSeverity(%q) = %q, want %q", tt.raw, got, tt.want) + } + } +} + +func TestSeverityErrorType(t *testing.T) { + tests := []struct { + severity SeverityLevel + want string + }{ + {SeverityCritical, "error"}, + {SeverityHigh, "error"}, + {SeverityMedium, "warning"}, + {SeverityLow, "info"}, + {SeverityInfo, "info"}, + {SeverityUnknown, "warning"}, + } + + for _, tt := range tests { + if got := tt.severity.ErrorType(); got != tt.want { + t.Errorf("%q.ErrorType() = %q, want %q", tt.severity, got, tt.want) + } + } +} + +func TestSeverityAtLeast(t *testing.T) { + if !SeverityCritical.AtLeast(SeverityHigh) { + t.Error("critical should be at least high") + } + if !SeverityHigh.AtLeast(SeverityHigh) { + t.Error("high should be at least high") + } + if SeverityMedium.AtLeast(SeverityHigh) { + t.Error("medium should not be at least high") + } + if SeverityUnknown.AtLeast(SeverityInfo) { + t.Error("unknown should not be at least info") + } +} + +func TestFindingCompilerError(t *testing.T) { + finding := Finding{ + RuleID: "template-injection", + Severity: SeverityHigh, + Message: "[High] template-injection: bad", + File: "workflow.lock.yml", + Line: 12, + Column: 4, + } + + compilerErr := finding.CompilerError() + if compilerErr.Type != "error" { + t.Errorf("expected type error, got %q", compilerErr.Type) + } + if compilerErr.Position.File != "workflow.lock.yml" || compilerErr.Position.Line != 12 || compilerErr.Position.Column != 4 { + t.Errorf("unexpected position: %+v", compilerErr.Position) + } + if compilerErr.Message != finding.Message { + t.Errorf("unexpected message: %q", compilerErr.Message) + } +} + +func TestFindingCompilerErrorDefaultsPosition(t *testing.T) { + compilerErr := Finding{Message: "no location"}.CompilerError() + if compilerErr.Position.Line != 1 || compilerErr.Position.Column != 1 { + t.Errorf("expected line and column to default to 1, got %+v", compilerErr.Position) + } +} + +func TestFormatMessage(t *testing.T) { + tests := []struct { + severity, rule, description string + want string + }{ + {"High", "RGS-001", "Unsafe runner", "[High] RGS-001: Unsafe runner"}, + {"warning", "rule", "", "[warning] rule"}, + {"", "rule", "detail", "rule: detail"}, + {"error", "", "detail", "[error] detail"}, + } + + for _, tt := range tests { + if got := FormatMessage(tt.severity, tt.rule, tt.description); got != tt.want { + t.Errorf("FormatMessage(%q, %q, %q) = %q, want %q", tt.severity, tt.rule, tt.description, got, tt.want) + } + } +} + +func TestSort(t *testing.T) { + findings := []Finding{ + {File: "b.yml", Line: 1, Severity: SeverityLow}, + {File: "a.yml", Line: 10, Column: 2, Severity: SeverityMedium}, + {File: "a.yml", Line: 10, Column: 1, Severity: SeverityLow}, + {File: "a.yml", Line: 2, Severity: SeverityInfo}, + } + + Sort(findings) + + want := []struct { + file string + line int + column int + }{ + {"a.yml", 2, 0}, + {"a.yml", 10, 1}, + {"a.yml", 10, 2}, + {"b.yml", 1, 0}, + } + + for i, w := range want { + got := findings[i] + if got.File != w.file || got.Line != w.line || got.Column != w.column { + t.Errorf("finding %d = %s:%d:%d, want %s:%d:%d", i, got.File, got.Line, got.Column, w.file, w.line, w.column) + } + } +} + +func TestSortOrdersBySeverityWithinSameLocation(t *testing.T) { + findings := []Finding{ + {File: "a.yml", Line: 1, Column: 1, Severity: SeverityLow, RuleID: "low"}, + {File: "a.yml", Line: 1, Column: 1, Severity: SeverityCritical, RuleID: "critical"}, + } + + Sort(findings) + + if findings[0].RuleID != "critical" { + t.Errorf("expected critical finding first, got %q", findings[0].RuleID) + } +} + +func TestCountAtLeast(t *testing.T) { + findings := []Finding{ + {Severity: SeverityCritical}, + {Severity: SeverityHigh}, + {Severity: SeverityMedium}, + {Severity: SeverityUnknown}, + } + + if got := CountAtLeast(findings, SeverityHigh); got != 2 { + t.Errorf("CountAtLeast(high) = %d, want 2", got) + } + if got := CountAtLeast(findings, SeverityInfo); got != 3 { + t.Errorf("CountAtLeast(info) = %d, want 3", got) + } +} + +func TestContextLines(t *testing.T) { + lines := []string{"one", "two", "three", "four", "five", "six"} + + got := ContextLines(lines, 4) + want := []string{"two", "three", "four", "five", "six"} + if len(got) != len(want) { + t.Fatalf("ContextLines(4) returned %d lines, want %d", len(got), len(want)) + } + for i := range want { + if got[i] != want[i] { + t.Errorf("ContextLines(4)[%d] = %q, want %q", i, got[i], want[i]) + } + } + + if got := ContextLines(lines, 1); len(got) != 3 { + t.Errorf("ContextLines(1) returned %d lines, want 3", len(got)) + } + if got := ContextLines(lines, 0); got != nil { + t.Errorf("ContextLines(0) = %v, want nil", got) + } + if got := ContextLines(lines, len(lines)+1); got != nil { + t.Errorf("ContextLines(out of range) = %v, want nil", got) + } + if got := ContextLines(nil, 1); got != nil { + t.Errorf("ContextLines(nil) = %v, want nil", got) + } +} + +func TestRender(t *testing.T) { + var buf bytes.Buffer + Render(&buf, []Finding{ + {Severity: SeverityHigh, Message: "boom", File: "a.yml", Line: 3, Column: 2}, + }) + + output := buf.String() + if !strings.Contains(output, "a.yml:3:2") { + t.Errorf("expected rendered position in output, got %q", output) + } + if !strings.Contains(output, "boom") { + t.Errorf("expected message in output, got %q", output) + } +} diff --git a/pkg/workflow/markdown_security_scanner.go b/pkg/workflow/markdown_security_scanner.go index d8d119eb69e..19ea22fd53a 100644 --- a/pkg/workflow/markdown_security_scanner.go +++ b/pkg/workflow/markdown_security_scanner.go @@ -30,6 +30,7 @@ import ( "unicode/utf8" "github.com/github/gh-aw/pkg/logger" + "github.com/github/gh-aw/pkg/scanfindings" "github.com/github/gh-aw/pkg/stringutil" ) @@ -61,6 +62,20 @@ type SecurityFinding struct { Snippet string // Short excerpt of the problematic content } +// ToFinding converts the security finding to the shared finding representation +// used by the scanner integrations. Markdown security findings are hard +// rejections, so they are always reported at high severity. +func (f SecurityFinding) ToFinding(filePath string) scanfindings.Finding { + return scanfindings.Finding{ + RuleID: string(f.Category), + Severity: scanfindings.SeverityHigh, + Message: fmt.Sprintf("[%s] %s", f.Category, f.Description), + File: filePath, + Line: f.Line, + Column: 1, + } +} + // countCategories counts unique security finding categories func countCategories(findings []SecurityFinding) int { categories := make(map[SecurityFindingCategory]bool) @@ -149,18 +164,17 @@ func FormatSecurityFindings(findings []SecurityFinding, filePath string) string // Format each finding using formatCompilerErrorWithPosition for consistency for _, f := range findings { - line := f.Line - if line <= 0 { - line = 1 // Default to line 1 if unknown - } + // Render through the shared finding representation so markdown security + // findings use the same severity vocabulary as the scanner integrations. + compilerErr := f.ToFinding(filePath).CompilerError() // Create a formatted error for this finding findingErr := formatCompilerErrorWithPosition( - filePath, - line, - 1, // Column 1 (we don't have column info) - "error", - fmt.Sprintf("[%s] %s", f.Category, f.Description), + compilerErr.Position.File, + compilerErr.Position.Line, + compilerErr.Position.Column, + compilerErr.Type, + compilerErr.Message, nil, ) From 30b9143b7250f7cbf0f2fb8699a7c2071a908c95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:00:07 +0000 Subject: [PATCH 4/9] Clarify CountAtLeast test expectation for unknown severities Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/scanfindings/scanfindings_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/scanfindings/scanfindings_test.go b/pkg/scanfindings/scanfindings_test.go index 754ec55a5cc..19e6a255e68 100644 --- a/pkg/scanfindings/scanfindings_test.go +++ b/pkg/scanfindings/scanfindings_test.go @@ -171,6 +171,7 @@ func TestCountAtLeast(t *testing.T) { if got := CountAtLeast(findings, SeverityHigh); got != 2 { t.Errorf("CountAtLeast(high) = %d, want 2", got) } + // Unknown severities rank below info and are therefore excluded. if got := CountAtLeast(findings, SeverityInfo); got != 3 { t.Errorf("CountAtLeast(info) = %d, want 3", got) } From d33d406ba3bf64b83399bb5d743d5e882c67fe4e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 22 Aug 2026 03:17:41 +0000 Subject: [PATCH 5/9] docs(adr): add draft ADR-54690 for shared scanfindings type Add draft Architecture Decision Record for the introduction of the shared Finding/SeverityLevel type across scanner integrations (PR #54690). Co-Authored-By: Claude Sonnet 4.6 --- ...nfindings-type-for-scanner-integrations.md | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md diff --git a/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md b/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md new file mode 100644 index 00000000000..9843fee6566 --- /dev/null +++ b/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md @@ -0,0 +1,46 @@ +# ADR-54690: Shared Finding/SeverityLevel Type Across Scanner Integrations + +**Date**: 2026-08-22 +**Status**: Draft +**Deciders**: Unknown + +--- + +### Context + +The codebase integrates nine security scanners (zizmor, poutine, grype, grant, runner-guard, yamllint, audit findings, validation issues, markdown security scanner). Each integration defined its own finding struct with a different severity vocabulary (`High`, `error`, `Negligible`, `note`, …) and its own location shape. As a result, severity classification, message formatting, context-line extraction, and console rendering were reimplemented per tool. This made adding new scanners expensive and meant severity inconsistencies (e.g., `low`-severity zizmor findings rendered as `warning` while grype rendered equivalent findings as `info`) accumulated silently. + +### Decision + +We will introduce a new `pkg/scanfindings` package that provides a shared `SeverityLevel` enum (`unknown` < `info` < `low` < `medium` < `high` < `critical`) and a shared `Finding` struct (`RuleID`, `Severity`, `Message`, `File`, `Line`, `Column`, `Context`). Each scanner integration retains its own native structs for JSON decoding, then maps onto `scanfindings.Finding` via a small `…FindingsToShared` adapter function. Severity classification, message formatting (`FormatMessage`), context-line extraction (`ContextLines`), rendering (`Render`), sorting (`Sort`), and counting (`CountAtLeast`) are implemented once in `pkg/scanfindings`. + +### Alternatives Considered + +#### Alternative 1: Extract a shared severity mapping function only + +Extract a single `ParseSeverity(raw string) string` helper returning a normalized string, keeping each tool's finding struct and rendering loop separate. This eliminates only the severity inconsistency while leaving context-line extraction, message building, and rendering duplicated across nine files. It is a smaller change but does not solve the maintenance problem that motivated this PR. + +#### Alternative 2: Define a `Finding` interface instead of a concrete struct + +Define a `Scanner` or `Finding` interface and let each integration implement it via its own native type. This avoids the explicit adapter functions and the coupling to a single shared struct, but adds indirection without eliminating boilerplate: every integration would still implement the same set of methods. The concrete-struct adapter approach chosen here produces less code overall and makes the mapping explicit and testable in isolation. + +### Consequences + +#### Positive +- Adding a scanner now requires only a field-mapping adapter; severity classification, rendering, sorting and counting are handled once. +- The severity vocabulary is normalized through `ParseSeverity`, eliminating per-tool inconsistencies in how identical native labels were classified. +- `audit_report.go`'s `Finding.Severity` is now a typed `SeverityLevel` rather than an unvalidated `string`, enabling compile-time checks in severity comparisons. +- Two duplicated rendering loops in `poutine.go` collapse into a single `poutineFindingsToShared` adapter. + +#### Negative +- All scanner integrations are now coupled to `pkg/scanfindings`; changes to the shared type's API propagate to every integration. +- The `Severity` field type on `audit_report.go`'s `Finding` struct changes from `string` to `SeverityLevel` — callers that compared `finding.Severity == "critical"` must now use `finding.Severity == scanfindings.SeverityCritical` or `.String()`. + +#### Neutral +- Low-severity zizmor and runner-guard findings now render as `info` (matching grype's behavior) rather than `warning` — this is an intentional alignment, not a regression. +- The `yamllintIssue` internal struct is removed; `parseYamllintLine` returns `scanfindings.Finding` directly. +- JSON serialization of `SeverityLevel` is unchanged: the underlying type is `string`, so existing JSON consumers reading `"severity"` fields see the same lowercase values. + +--- + +*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* From 286aab46c6f2a31367411b4381f9d9092854d35e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 05:24:54 +0000 Subject: [PATCH 6/9] Fix shared finding adapter regressions Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- ...nfindings-type-for-scanner-integrations.md | 6 +++--- pkg/cli/poutine.go | 2 +- pkg/cli/poutine_test.go | 17 +++++++++++++++++ pkg/cli/validation_issue.go | 13 +++++-------- pkg/cli/validation_issue_test.go | 19 +++++++++++++++++++ pkg/scanfindings/scanfindings.go | 12 +++++++----- pkg/scanfindings/scanfindings_test.go | 10 ++++++++-- 7 files changed, 60 insertions(+), 19 deletions(-) diff --git a/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md b/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md index 9843fee6566..5f41c3c9c0c 100644 --- a/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md +++ b/docs/adr/54690-shared-scanfindings-type-for-scanner-integrations.md @@ -1,8 +1,8 @@ # ADR-54690: Shared Finding/SeverityLevel Type Across Scanner Integrations **Date**: 2026-08-22 -**Status**: Draft -**Deciders**: Unknown +**Status**: Accepted +**Deciders**: copilot-swe-agent (PR author), gh-aw maintainers --- @@ -43,4 +43,4 @@ Define a `Scanner` or `Finding` interface and let each integration implement it --- -*ADR created by [adr-writer agent]. Review and finalize before changing status from Draft to Accepted.* +*Accepted on 2026-08-22 after validating the shared type and all scanner adapters.* diff --git a/pkg/cli/poutine.go b/pkg/cli/poutine.go index 8658ac10a52..fe9a39c273a 100644 --- a/pkg/cli/poutine.go +++ b/pkg/cli/poutine.go @@ -502,7 +502,7 @@ func poutineFindingsToShared(findings []poutineFinding, rules poutineRules, file RuleID: finding.RuleID, Severity: scanfindings.ParseSeverity(severityLabel), Message: message, - File: filePath, + File: firstNonEmpty(finding.Meta.Path, filePath), Line: lineNum, Column: 1, // poutine doesn't provide column info Context: scanfindings.ContextLines(fileLines, lineNum), diff --git a/pkg/cli/poutine_test.go b/pkg/cli/poutine_test.go index 1fb60d0cb6d..2fd4d3b4c0b 100644 --- a/pkg/cli/poutine_test.go +++ b/pkg/cli/poutine_test.go @@ -372,3 +372,20 @@ func TestPoutineImageIsPinnedByDigest(t *testing.T) { t.Errorf("PoutineImage must be pinned by digest, got %q", PoutineImage) } } + +func TestPoutineFindingsToSharedUsesFindingPath(t *testing.T) { + finding := poutineFinding{RuleID: "injection"} + finding.Meta.Path = "nested/workflow.lock.yml" + finding.Meta.Line = 2 + + findings := poutineFindingsToShared([]poutineFinding{finding}, poutineRules{ + "injection": {Level: "error", Title: "Injection"}, + }, "workflow.lock.yml", []string{"one", "two", "three"}) + + if len(findings) != 1 { + t.Fatalf("got %d findings, want 1", len(findings)) + } + if findings[0].File != "nested/workflow.lock.yml" { + t.Errorf("File = %q, want finding path", findings[0].File) + } +} diff --git a/pkg/cli/validation_issue.go b/pkg/cli/validation_issue.go index bbeb9cf07ef..0b60e5be84f 100644 --- a/pkg/cli/validation_issue.go +++ b/pkg/cli/validation_issue.go @@ -10,16 +10,13 @@ type ValidationIssue struct { File string `json:"file,omitempty"` } -// Severity maps the issue type ("error", "warning", ...) onto the shared -// severity vocabulary used by the scanner integrations. -func (v ValidationIssue) Severity() scanfindings.SeverityLevel { - return scanfindings.ParseSeverity(v.Type) -} - // ToFinding converts the validation issue to the shared finding representation. -func (v ValidationIssue) ToFinding() scanfindings.Finding { +// The caller supplies severity because Type is a diagnostic category, not a +// severity level. +func (v ValidationIssue) ToFinding(severity scanfindings.SeverityLevel) scanfindings.Finding { return scanfindings.Finding{ - Severity: v.Severity(), + RuleID: v.Type, + Severity: severity, Message: v.Message, File: v.File, Line: v.Line, diff --git a/pkg/cli/validation_issue_test.go b/pkg/cli/validation_issue_test.go index 7a232cda012..5d857fd0d39 100644 --- a/pkg/cli/validation_issue_test.go +++ b/pkg/cli/validation_issue_test.go @@ -5,6 +5,8 @@ package cli import ( "encoding/json" "testing" + + "github.com/github/gh-aw/pkg/scanfindings" ) func TestValidationIssueJSONCompatibility(t *testing.T) { @@ -78,3 +80,20 @@ func TestValidationIssueJSONCompatibility(t *testing.T) { t.Fatalf("expected audit line to be serialized, got %#v", auditIssue["line"]) } } + +func TestValidationIssueToFindingUsesSuppliedSeverity(t *testing.T) { + issue := ValidationIssue{ + Type: "schema_validation", + Message: "Unknown property", + File: "workflow.md", + Line: 5, + } + + finding := issue.ToFinding(scanfindings.SeverityHigh) + if finding.RuleID != issue.Type { + t.Errorf("RuleID = %q, want %q", finding.RuleID, issue.Type) + } + if finding.Severity != scanfindings.SeverityHigh { + t.Errorf("Severity = %q, want %q", finding.Severity, scanfindings.SeverityHigh) + } +} diff --git a/pkg/scanfindings/scanfindings.go b/pkg/scanfindings/scanfindings.go index d8b161134f7..7c6a68ecd4b 100644 --- a/pkg/scanfindings/scanfindings.go +++ b/pkg/scanfindings/scanfindings.go @@ -201,16 +201,18 @@ func CountAtLeast(findings []Finding, min SeverityLevel) int { return count } -// ContextLines returns up to two source lines before and after the 1-based line -// number, used to display a finding in context. It returns nil when the line is -// out of range for the provided file lines. +// ContextLines returns a symmetric window of up to two source lines before and +// after the 1-based line number. The window shrinks at file boundaries to keep +// the target line at its midpoint for context rendering. It returns nil when +// the line is out of range for the provided file lines. func ContextLines(fileLines []string, line int) []string { if len(fileLines) == 0 || line <= 0 || line > len(fileLines) { return nil } - start := max(1, line-2) - end := min(len(fileLines), line+2) + window := min(2, line-1, len(fileLines)-line) + start := line - window + end := line + window context := make([]string, 0, end-start+1) for i := start; i <= end; i++ { diff --git a/pkg/scanfindings/scanfindings_test.go b/pkg/scanfindings/scanfindings_test.go index 19e6a255e68..5e8483e7b9c 100644 --- a/pkg/scanfindings/scanfindings_test.go +++ b/pkg/scanfindings/scanfindings_test.go @@ -191,8 +191,14 @@ func TestContextLines(t *testing.T) { } } - if got := ContextLines(lines, 1); len(got) != 3 { - t.Errorf("ContextLines(1) returned %d lines, want 3", len(got)) + if got := ContextLines(lines, 1); len(got) != 1 || got[0] != "one" { + t.Errorf("ContextLines(1) = %v, want [one]", got) + } + if got := ContextLines(lines, 2); len(got) != 3 || got[1] != "two" { + t.Errorf("ContextLines(2) = %v, want [one two three]", got) + } + if got := ContextLines(lines, len(lines)); len(got) != 1 || got[0] != "six" { + t.Errorf("ContextLines(last) = %v, want [six]", got) } if got := ContextLines(lines, 0); got != nil { t.Errorf("ContextLines(0) = %v, want nil", got) From 9ebdde90ddad853ab7bc45b847c2dd4d8cbf2b8a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:31:17 +0000 Subject: [PATCH 7/9] Investigate CI failure Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentic_commands.yml | 7 ++++--- pkg/workflow/schemas/github-workflow.json | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/agentic_commands.yml b/.github/workflows/agentic_commands.yml index dbb43b5ca6a..e9f5699b47d 100644 --- a/.github/workflows/agentic_commands.yml +++ b/.github/workflows/agentic_commands.yml @@ -1,4 +1,4 @@ -# gh-aw-commands: {"payload_version":"v1","schema_version":"v1","compiler_version":"dev","commands":["*","ace","approach-validator","archie","cloclo","craft","dependabot-burner","grumpy","matt","mergefest","nit","plan","poem-bot","ponytail","review","ruflo","scout","security-review","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","souschef","squad-plan","summarize","tidy","unbloat"],"workflows":["ace-editor","approach-validator","archie","ci-doctor","cloclo","craft","dependabot-burner","design-decision-gate","dev","grumpy-reviewer","mattpocock-skills-reviewer","mergefest","necromancer","pdf-summary","plan","poem-bot","ponytail-reviewer","pr-code-quality-reviewer","pr-nitpick-reviewer","pr-sous-chef","ruflo-backed-task","scout","security-review","skillet","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","squad-plan","test-quality-sentinel","tidy","unbloat-docs"]} +# gh-aw-commands: {"payload_version":"v1","schema_version":"v1","compiler_version":"dev","commands":["*","ace","approach-validator","archie","cloclo","craft","dependabot-burner","grumpy","matt","mergefest","nit","plan","poem-bot","ponytail","review","ruflo","scout","security-review","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-drive","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","souschef","squad-plan","summarize","tidy","unbloat"],"workflows":["ace-editor","approach-validator","archie","ci-doctor","cloclo","craft","dependabot-burner","design-decision-gate","dev","grumpy-reviewer","mattpocock-skills-reviewer","mergefest","necromancer","pdf-summary","plan","poem-bot","ponytail-reviewer","pr-code-quality-reviewer","pr-nitpick-reviewer","pr-sous-chef","ruflo-backed-task","scout","security-review","skillet","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-drive","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","squad-plan","test-quality-sentinel","tidy","unbloat-docs"]} # Routing summary (sorted): # slash commands: # /* -> skillet [pull_request_comment,pull_request_review_comment] reaction=eyes @@ -43,6 +43,7 @@ # /smoke-crush -> smoke-crush [issue_comment,issues,pull_request,pull_request_comment] reaction=eyes # /smoke-cursor -> smoke-cursor [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-deepseek-harness -> smoke-deepseek-harness [issue_comment,issues,pull_request,pull_request_comment] reaction=eyes +# /smoke-drive -> smoke-drive [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-gemini -> smoke-gemini [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-github-claude -> smoke-github-claude [pull_request,pull_request_comment] reaction=eyes # /smoke-goose -> smoke-goose [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket @@ -141,9 +142,9 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # runner-guard:ignore RGS-016 -- routing tables below contain emoji variation selectors (U+FE0F) and zero-width joiners (U+200D) used to render standard emoji sequences, not steganographic payloads. env: - GH_AW_SLASH_ROUTING: '{"*":[{"workflow":"skillet","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🍳","status_comment":true}],"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"✏️","status_comment":true}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🏛️","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"📊","status_comment":true}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes","emoji":"✍️","status_comment":true}],"dependabot-burner":[{"workflow":"dependabot-burner","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔥","status_comment":true}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🔀","status_comment":true}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes","emoji":"📋","status_comment":true}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes","emoji":"🎭","status_comment":true}],"ponytail":[{"workflow":"ponytail-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"✂️","status_comment":true}],"review":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🏗️","status_comment":true},{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true},{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"ruflo":[{"workflow":"ruflo-backed-task","events":["issue_comment"],"ai_reaction":"eyes","status_comment":true}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔭","status_comment":true}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔒","status_comment":true}],"smoke-agent-all-merged":[{"workflow":"smoke-agent-all-merged","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-all-none":[{"workflow":"smoke-agent-all-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-approved":[{"workflow":"smoke-agent-public-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-none":[{"workflow":"smoke-agent-public-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-scoped-approved":[{"workflow":"smoke-agent-scoped-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-aider":[{"workflow":"smoke-aider","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧑‍✈️","status_comment":true}],"smoke-call-workflow":[{"workflow":"smoke-call-workflow","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-checkout-pr-dispatch":[{"workflow":"smoke-checkout-pr-dispatch","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-claude":[{"workflow":"smoke-claude","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"heart","emoji":"🧪","status_comment":true}],"smoke-claude-on-copilot":[{"workflow":"smoke-claude-on-copilot","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-codex":[{"workflow":"smoke-codex","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"hooray","emoji":"🧪","status_comment":true}],"smoke-copilot":[{"workflow":"smoke-copilot","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-apikey":[{"workflow":"smoke-copilot-aoai-apikey","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-entra":[{"workflow":"smoke-copilot-aoai-entra","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-arm":[{"workflow":"smoke-copilot-arm","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-mai":[{"workflow":"smoke-copilot-mai","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"⚡","status_comment":true}],"smoke-copilot-sdk":[{"workflow":"smoke-copilot-sdk","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🔬","status_comment":true}],"smoke-copilot-small":[{"workflow":"smoke-copilot-small","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🪶","status_comment":true}],"smoke-create-cross-repo-pr":[{"workflow":"smoke-create-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-crush":[{"workflow":"smoke-crush","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-cursor":[{"workflow":"smoke-cursor","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🖱️","status_comment":true}],"smoke-deepseek-harness":[{"workflow":"smoke-deepseek-harness","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-gemini":[{"workflow":"smoke-gemini","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-github-claude":[{"workflow":"smoke-github-claude","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-goose":[{"workflow":"smoke-goose","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🪿","status_comment":true}],"smoke-kiro":[{"workflow":"smoke-kiro","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧭","status_comment":true}],"smoke-multi-pr":[{"workflow":"smoke-multi-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-opencode":[{"workflow":"smoke-opencode","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-otel-backends":[{"workflow":"smoke-otel-backends","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pi":[{"workflow":"smoke-pi","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-project":[{"workflow":"smoke-project","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pydantic":[{"workflow":"smoke-pydantic","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🐍","status_comment":true}],"smoke-service-ports":[{"workflow":"smoke-service-ports","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-temporary-id":[{"workflow":"smoke-temporary-id","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-test-tools":[{"workflow":"smoke-test-tools","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-update-cross-repo-pr":[{"workflow":"smoke-update-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"souschef":[{"workflow":"pr-sous-chef","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"👨‍🍳","status_comment":true}],"squad-plan":[{"workflow":"squad-plan","events":["issue_comment"],"ai_reaction":"eyes","emoji":"🧑‍🤝‍🧑","status_comment":true}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes","emoji":"📄","status_comment":true}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🧹","status_comment":true}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"📝","status_comment":true}]}' + GH_AW_SLASH_ROUTING: '{"*":[{"workflow":"skillet","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🍳","status_comment":true}],"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"✏️","status_comment":true}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🏛️","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"📊","status_comment":true}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes","emoji":"✍️","status_comment":true}],"dependabot-burner":[{"workflow":"dependabot-burner","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔥","status_comment":true}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🔀","status_comment":true}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes","emoji":"📋","status_comment":true}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes","emoji":"🎭","status_comment":true}],"ponytail":[{"workflow":"ponytail-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"✂️","status_comment":true}],"review":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🏗️","status_comment":true},{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true},{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"ruflo":[{"workflow":"ruflo-backed-task","events":["issue_comment"],"ai_reaction":"eyes","status_comment":true}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔭","status_comment":true}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔒","status_comment":true}],"smoke-agent-all-merged":[{"workflow":"smoke-agent-all-merged","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-all-none":[{"workflow":"smoke-agent-all-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-approved":[{"workflow":"smoke-agent-public-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-none":[{"workflow":"smoke-agent-public-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-scoped-approved":[{"workflow":"smoke-agent-scoped-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-aider":[{"workflow":"smoke-aider","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧑‍✈️","status_comment":true}],"smoke-call-workflow":[{"workflow":"smoke-call-workflow","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-checkout-pr-dispatch":[{"workflow":"smoke-checkout-pr-dispatch","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-claude":[{"workflow":"smoke-claude","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"heart","emoji":"🧪","status_comment":true}],"smoke-claude-on-copilot":[{"workflow":"smoke-claude-on-copilot","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-codex":[{"workflow":"smoke-codex","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"hooray","emoji":"🧪","status_comment":true}],"smoke-copilot":[{"workflow":"smoke-copilot","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-apikey":[{"workflow":"smoke-copilot-aoai-apikey","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-entra":[{"workflow":"smoke-copilot-aoai-entra","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-arm":[{"workflow":"smoke-copilot-arm","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-mai":[{"workflow":"smoke-copilot-mai","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"⚡","status_comment":true}],"smoke-copilot-sdk":[{"workflow":"smoke-copilot-sdk","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🔬","status_comment":true}],"smoke-copilot-small":[{"workflow":"smoke-copilot-small","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🪶","status_comment":true}],"smoke-create-cross-repo-pr":[{"workflow":"smoke-create-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-crush":[{"workflow":"smoke-crush","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-cursor":[{"workflow":"smoke-cursor","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🖱️","status_comment":true}],"smoke-deepseek-harness":[{"workflow":"smoke-deepseek-harness","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-drive":[{"workflow":"smoke-drive","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"💾","status_comment":true}],"smoke-gemini":[{"workflow":"smoke-gemini","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-github-claude":[{"workflow":"smoke-github-claude","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-goose":[{"workflow":"smoke-goose","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🪿","status_comment":true}],"smoke-kiro":[{"workflow":"smoke-kiro","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧭","status_comment":true}],"smoke-multi-pr":[{"workflow":"smoke-multi-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-opencode":[{"workflow":"smoke-opencode","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-otel-backends":[{"workflow":"smoke-otel-backends","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pi":[{"workflow":"smoke-pi","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-project":[{"workflow":"smoke-project","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pydantic":[{"workflow":"smoke-pydantic","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🐍","status_comment":true}],"smoke-service-ports":[{"workflow":"smoke-service-ports","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-temporary-id":[{"workflow":"smoke-temporary-id","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-test-tools":[{"workflow":"smoke-test-tools","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-update-cross-repo-pr":[{"workflow":"smoke-update-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"souschef":[{"workflow":"pr-sous-chef","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"👨‍🍳","status_comment":true}],"squad-plan":[{"workflow":"squad-plan","events":["issue_comment"],"ai_reaction":"eyes","emoji":"🧑‍🤝‍🧑","status_comment":true}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes","emoji":"📄","status_comment":true}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🧹","status_comment":true}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"📝","status_comment":true}]}' GH_AW_LABEL_ROUTING: '{"approach-proposal":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"ci-doctor":[{"workflow":"ci-doctor","events":["pull_request"],"ai_reaction":"eyes","emoji":"🏥","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","issues","pull_request"],"ai_reaction":"eyes","emoji":"📊","status_comment":true}],"dev":[{"workflow":"dev","events":["discussion","issues","pull_request"],"ai_reaction":"eyes","emoji":"💻","status_comment":true}],"necromancer":[{"workflow":"necromancer","events":["pull_request"],"ai_reaction":"eyes","emoji":"💀","status_comment":true}],"needs-design":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"smoke":[{"workflow":"smoke-copilot","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true},{"workflow":"smoke-copilot-aoai-apikey","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true},{"workflow":"smoke-copilot-aoai-entra","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true},{"workflow":"smoke-copilot-mai","events":["pull_request"],"ai_reaction":"eyes","emoji":"⚡","status_comment":true},{"workflow":"smoke-copilot-small","events":["pull_request"],"ai_reaction":"eyes","emoji":"🪶","status_comment":true},{"workflow":"smoke-otel-backends","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-sdk":[{"workflow":"smoke-copilot-sdk","events":["pull_request"],"ai_reaction":"eyes","emoji":"🔬","status_comment":true}]}' - GH_AW_HELP_COMMANDS: '[{"command":"*","description":"Reviews pull requests by mapping any slash command to a matching repository skill under .github/skills","centralized":true,"decentralized":false,"source_file":"skillet"},{"command":"ace","description":"Generates an ACE editor session link when invoked with /ace command on pull request comments","centralized":true,"decentralized":false,"source_file":"ace-editor"},{"command":"approach-validator","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":true,"decentralized":false,"source_file":"approach-validator"},{"command":"archie","description":"Generates Mermaid diagrams to visualize issue and pull request relationships when invoked with the /archie command","centralized":true,"decentralized":false,"source_file":"archie"},{"command":"cloclo","centralized":true,"decentralized":false,"source_file":"cloclo"},{"command":"craft","description":"Generates new agentic workflow markdown files based on user requests when invoked with /craft command","centralized":true,"decentralized":false,"source_file":"craft"},{"command":"dependabot-burner","description":"Runs one grouped Dependabot remediation wave from schedule, manual dispatch, or /dependabot-burner on pull requests","centralized":true,"decentralized":false,"source_file":"dependabot-burner"},{"command":"grumpy","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Performs critical code review with a focus on edge cases, potential bugs, and code quality issues","centralized":true,"decentralized":false,"source_file":"grumpy-reviewer"},{"command":"matt","description":"Reviews pull requests using Matt Pocock''s engineering skills to provide targeted, high-quality improvement suggestions based on the type of changes","centralized":true,"decentralized":false,"source_file":"mattpocock-skills-reviewer"},{"command":"mergefest","description":"Automatically merges the main branch into pull request branches when invoked with /mergefest command","centralized":true,"decentralized":false,"source_file":"mergefest"},{"command":"nit","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Provides detailed nitpicky code review focusing on style, best practices, and minor improvements","centralized":true,"decentralized":false,"source_file":"pr-nitpick-reviewer"},{"command":"plan","description":"Generates project plans and task breakdowns when invoked with /plan command in issues or PRs","centralized":true,"decentralized":false,"source_file":"plan"},{"command":"poem-bot","description":"Generates creative poems on specified themes when invoked with /poem-bot command","centralized":true,"decentralized":false,"source_file":"poem-bot"},{"command":"ponytail","description":"Reviews pull requests for unnecessary complexity using Ponytail","centralized":true,"decentralized":false,"source_file":"ponytail-reviewer"},{"command":"q","description":"Intelligent assistant that answers questions, analyzes repositories, and can create PRs for workflow optimizations","centralized":false,"decentralized":true,"source_file":"q"},{"command":"review","description":"Enforces Architecture Decision Records (ADRs) before implementation work can merge, detecting missing design decisions and generating draft ADRs using AI analysis","centralized":true,"decentralized":false,"source_file":"design-decision-gate"},{"command":"ruflo","description":"Runs a repository task inside GitHub Agentic Workflows while delegating inner planning and coordination to Ruflo","centralized":true,"decentralized":false,"source_file":"ruflo-backed-task"},{"command":"scout","description":"Performs deep research investigations using web search to gather and synthesize comprehensive information on any topic","centralized":true,"decentralized":false,"source_file":"scout"},{"command":"security-review","description":"Security-focused AI agent that reviews pull requests to identify changes that could weaken security posture or extend AWF boundaries","centralized":true,"decentralized":false,"source_file":"security-review"},{"command":"smoke-agent-all-merged","description":"Guard policy smoke test: repos=all, min-integrity=merged (most restrictive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-merged"},{"command":"smoke-agent-all-none","description":"Guard policy smoke test: repos=all, min-integrity=none (most permissive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-none"},{"command":"smoke-agent-public-approved","description":"Smoke test that validates assign-to-agent with the agentic-workflows custom agent","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-approved"},{"command":"smoke-agent-public-none","description":"Guard policy smoke test: repos=public, min-integrity=none","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-none"},{"command":"smoke-agent-scoped-approved","description":"Guard policy smoke test: repos=[github/gh-aw, github/*], min-integrity=approved (scoped patterns)","centralized":true,"decentralized":false,"source_file":"smoke-agent-scoped-approved"},{"command":"smoke-aider","description":"Smoke test workflow that validates Aider engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-aider"},{"command":"smoke-call-workflow","description":"Smoke test for the call-workflow safe output - orchestrator that calls a worker via workflow_call at compile-time fan-out","centralized":true,"decentralized":false,"source_file":"smoke-call-workflow"},{"command":"smoke-checkout-pr-dispatch","description":"Integration test validating that workflow_dispatch events with aw_context.item_type == ''pull_request'' correctly check out the PR branch","centralized":true,"decentralized":false,"source_file":"smoke-checkout-pr-dispatch"},{"command":"smoke-claude","description":"Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-claude"},{"command":"smoke-claude-on-copilot","description":"Smoke test for Claude engine on GitHub Inference that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-claude-on-copilot"},{"command":"smoke-codex","description":"Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-codex"},{"command":"smoke-copilot","description":"Smoke Copilot","centralized":true,"decentralized":false,"source_file":"smoke-copilot"},{"command":"smoke-copilot-aoai-apikey","description":"Smoke Copilot - AOAI (apikey)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-copilot-aoai-entra","description":"Smoke Copilot - AOAI (Entra)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-entra"},{"command":"smoke-copilot-arm","description":"Smoke Copilot ARM64","centralized":true,"decentralized":false,"source_file":"smoke-copilot-arm"},{"command":"smoke-copilot-mai","description":"Smoke test for MAI-Code-1-Flash (mai-code-1-flash-picker) — pricing: $0.75/M input, $0.075/M cached, $4.50/M output","centralized":true,"decentralized":false,"source_file":"smoke-copilot-mai"},{"command":"smoke-copilot-sdk","description":"Smoke Copilot SDK","centralized":true,"decentralized":false,"source_file":"smoke-copilot-sdk"},{"command":"smoke-copilot-small","description":"Smoke Copilot Small","centralized":true,"decentralized":false,"source_file":"smoke-copilot-small"},{"command":"smoke-create-cross-repo-pr","description":"Smoke test validating cross-repo pull request creation in github/gh-aw-side-repo","centralized":true,"decentralized":false,"source_file":"smoke-create-cross-repo-pr"},{"command":"smoke-crush","description":"Smoke test workflow that validates Crush engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-crush"},{"command":"smoke-cursor","description":"Smoke test workflow that validates Cursor engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-cursor"},{"command":"smoke-deepseek-harness","description":"Smoke test workflow that validates DeepSeek Harness engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-deepseek-harness"},{"command":"smoke-gemini","description":"Smoke test workflow that validates Gemini engine functionality twice daily","centralized":true,"decentralized":false,"source_file":"smoke-gemini"},{"command":"smoke-github-claude","description":"Smoke test for Claude engine using GitHub provider that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-github-claude"},{"command":"smoke-goose","description":"Smoke test workflow that validates Goose engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-goose"},{"command":"smoke-kiro","description":"Smoke test workflow that validates Kiro engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-kiro"},{"command":"smoke-multi-pr","description":"Test creating multiple pull requests in a single workflow run","centralized":true,"decentralized":false,"source_file":"smoke-multi-pr"},{"command":"smoke-opencode","description":"Smoke test workflow that validates OpenCode engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-opencode"},{"command":"smoke-otel-backends","description":"Smoke test that validates OTEL span export and query access for Sentry, Grafana, and Datadog","centralized":true,"decentralized":false,"source_file":"smoke-otel-backends"},{"command":"smoke-pi","description":"Smoke test workflow that validates Pi engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pi"},{"command":"smoke-project","description":"Smoke Project - Test project operations","centralized":true,"decentralized":false,"source_file":"smoke-project"},{"command":"smoke-pydantic","description":"Smoke test workflow that validates Pydantic AI engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pydantic"},{"command":"smoke-service-ports","description":"Smoke test to validate --allow-host-service-ports with Redis service container","centralized":true,"decentralized":false,"source_file":"smoke-service-ports"},{"command":"smoke-temporary-id","description":"Test temporary ID functionality for issue chaining and cross-references","centralized":true,"decentralized":false,"source_file":"smoke-temporary-id"},{"command":"smoke-test-tools","description":"Smoke test to validate common development tools are available in the agent container","centralized":true,"decentralized":false,"source_file":"smoke-test-tools"},{"command":"smoke-update-cross-repo-pr","description":"Smoke test validating cross-repo pull request updates in github/gh-aw-side-repo by adding lines from Homer''s Odyssey to the README","centralized":true,"decentralized":false,"source_file":"smoke-update-cross-repo-pr"},{"command":"souschef","description":"Keeps open non-draft PRs moving toward maintainer investigation by posting targeted Copilot nudges","centralized":true,"decentralized":false,"source_file":"pr-sous-chef"},{"command":"squad","description":"Cast, connect, or adopt a Squad AI team for your repository","centralized":false,"decentralized":true,"source_file":"squad"},{"command":"squad-plan","description":"Uses Squad to plan an issue from the /squad-plan slash command and create Copilot-ready sub-issues","centralized":true,"decentralized":false,"source_file":"squad-plan"},{"command":"summarize","description":"pdf summarizer","centralized":true,"decentralized":false,"source_file":"pdf-summary"},{"command":"tidy","description":"Automatically formats and tidies code files (Go, JS, TypeScript) on schedule or command","centralized":true,"decentralized":false,"source_file":"tidy"},{"command":"unbloat","description":"Reviews and simplifies documentation by reducing verbosity while maintaining clarity and completeness","centralized":true,"decentralized":false,"source_file":"unbloat-docs"},{"command":"approach-proposal","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"ci-doctor","description":"Investigates failed CI workflows to identify root causes and patterns, creating issues with diagnostic information; also reviews PR check failures when the ci-doctor label is applied","centralized":false,"decentralized":false,"label":true,"source_file":"ci-doctor"},{"command":"cloclo","centralized":false,"decentralized":false,"label":true,"source_file":"cloclo"},{"command":"dev","description":"Daily status report for gh-aw project","centralized":false,"decentralized":false,"label":true,"source_file":"dev"},{"command":"necromancer","description":"Investigates merge-ready pull requests, traces root-cause issues, and adds regression tests before merge","centralized":false,"decentralized":false,"label":true,"source_file":"necromancer"},{"command":"needs-design","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"smoke","description":"Smoke Copilot - AOAI (apikey)","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-sdk","description":"Smoke Copilot SDK","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-sdk"}]' + GH_AW_HELP_COMMANDS: '[{"command":"*","description":"Reviews pull requests by mapping any slash command to a matching repository skill under .github/skills","centralized":true,"decentralized":false,"source_file":"skillet"},{"command":"ace","description":"Generates an ACE editor session link when invoked with /ace command on pull request comments","centralized":true,"decentralized":false,"source_file":"ace-editor"},{"command":"approach-validator","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":true,"decentralized":false,"source_file":"approach-validator"},{"command":"archie","description":"Generates Mermaid diagrams to visualize issue and pull request relationships when invoked with the /archie command","centralized":true,"decentralized":false,"source_file":"archie"},{"command":"cloclo","centralized":true,"decentralized":false,"source_file":"cloclo"},{"command":"craft","description":"Generates new agentic workflow markdown files based on user requests when invoked with /craft command","centralized":true,"decentralized":false,"source_file":"craft"},{"command":"dependabot-burner","description":"Runs one grouped Dependabot remediation wave from schedule, manual dispatch, or /dependabot-burner on pull requests","centralized":true,"decentralized":false,"source_file":"dependabot-burner"},{"command":"grumpy","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Performs critical code review with a focus on edge cases, potential bugs, and code quality issues","centralized":true,"decentralized":false,"source_file":"grumpy-reviewer"},{"command":"matt","description":"Reviews pull requests using Matt Pocock''s engineering skills to provide targeted, high-quality improvement suggestions based on the type of changes","centralized":true,"decentralized":false,"source_file":"mattpocock-skills-reviewer"},{"command":"mergefest","description":"Automatically merges the main branch into pull request branches when invoked with /mergefest command","centralized":true,"decentralized":false,"source_file":"mergefest"},{"command":"nit","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Provides detailed nitpicky code review focusing on style, best practices, and minor improvements","centralized":true,"decentralized":false,"source_file":"pr-nitpick-reviewer"},{"command":"plan","description":"Generates project plans and task breakdowns when invoked with /plan command in issues or PRs","centralized":true,"decentralized":false,"source_file":"plan"},{"command":"poem-bot","description":"Generates creative poems on specified themes when invoked with /poem-bot command","centralized":true,"decentralized":false,"source_file":"poem-bot"},{"command":"ponytail","description":"Reviews pull requests for unnecessary complexity using Ponytail","centralized":true,"decentralized":false,"source_file":"ponytail-reviewer"},{"command":"q","description":"Intelligent assistant that answers questions, analyzes repositories, and can create PRs for workflow optimizations","centralized":false,"decentralized":true,"source_file":"q"},{"command":"review","description":"Enforces Architecture Decision Records (ADRs) before implementation work can merge, detecting missing design decisions and generating draft ADRs using AI analysis","centralized":true,"decentralized":false,"source_file":"design-decision-gate"},{"command":"ruflo","description":"Runs a repository task inside GitHub Agentic Workflows while delegating inner planning and coordination to Ruflo","centralized":true,"decentralized":false,"source_file":"ruflo-backed-task"},{"command":"scout","description":"Performs deep research investigations using web search to gather and synthesize comprehensive information on any topic","centralized":true,"decentralized":false,"source_file":"scout"},{"command":"security-review","description":"Security-focused AI agent that reviews pull requests to identify changes that could weaken security posture or extend AWF boundaries","centralized":true,"decentralized":false,"source_file":"security-review"},{"command":"smoke-agent-all-merged","description":"Guard policy smoke test: repos=all, min-integrity=merged (most restrictive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-merged"},{"command":"smoke-agent-all-none","description":"Guard policy smoke test: repos=all, min-integrity=none (most permissive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-none"},{"command":"smoke-agent-public-approved","description":"Smoke test that validates assign-to-agent with the agentic-workflows custom agent","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-approved"},{"command":"smoke-agent-public-none","description":"Guard policy smoke test: repos=public, min-integrity=none","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-none"},{"command":"smoke-agent-scoped-approved","description":"Guard policy smoke test: repos=[github/gh-aw, github/*], min-integrity=approved (scoped patterns)","centralized":true,"decentralized":false,"source_file":"smoke-agent-scoped-approved"},{"command":"smoke-aider","description":"Smoke test workflow that validates Aider engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-aider"},{"command":"smoke-call-workflow","description":"Smoke test for the call-workflow safe output - orchestrator that calls a worker via workflow_call at compile-time fan-out","centralized":true,"decentralized":false,"source_file":"smoke-call-workflow"},{"command":"smoke-checkout-pr-dispatch","description":"Integration test validating that workflow_dispatch events with aw_context.item_type == ''pull_request'' correctly check out the PR branch","centralized":true,"decentralized":false,"source_file":"smoke-checkout-pr-dispatch"},{"command":"smoke-claude","description":"Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-claude"},{"command":"smoke-claude-on-copilot","description":"Smoke test for Claude engine on GitHub Inference that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-claude-on-copilot"},{"command":"smoke-codex","description":"Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-codex"},{"command":"smoke-copilot","description":"Smoke Copilot","centralized":true,"decentralized":false,"source_file":"smoke-copilot"},{"command":"smoke-copilot-aoai-apikey","description":"Smoke Copilot - AOAI (apikey)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-copilot-aoai-entra","description":"Smoke Copilot - AOAI (Entra)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-entra"},{"command":"smoke-copilot-arm","description":"Smoke Copilot ARM64","centralized":true,"decentralized":false,"source_file":"smoke-copilot-arm"},{"command":"smoke-copilot-mai","description":"Smoke test for MAI-Code-1-Flash (mai-code-1-flash-picker) — pricing: $0.75/M input, $0.075/M cached, $4.50/M output","centralized":true,"decentralized":false,"source_file":"smoke-copilot-mai"},{"command":"smoke-copilot-sdk","description":"Smoke Copilot SDK","centralized":true,"decentralized":false,"source_file":"smoke-copilot-sdk"},{"command":"smoke-copilot-small","description":"Smoke Copilot Small","centralized":true,"decentralized":false,"source_file":"smoke-copilot-small"},{"command":"smoke-create-cross-repo-pr","description":"Smoke test validating cross-repo pull request creation in github/gh-aw-side-repo","centralized":true,"decentralized":false,"source_file":"smoke-create-cross-repo-pr"},{"command":"smoke-crush","description":"Smoke test workflow that validates Crush engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-crush"},{"command":"smoke-cursor","description":"Smoke test workflow that validates Cursor engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-cursor"},{"command":"smoke-deepseek-harness","description":"Smoke test workflow that validates DeepSeek Harness engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-deepseek-harness"},{"command":"smoke-drive","description":"Smoke test workflow that validates experimental GitHub Drives memory","centralized":true,"decentralized":false,"source_file":"smoke-drive"},{"command":"smoke-gemini","description":"Smoke test workflow that validates Gemini engine functionality twice daily","centralized":true,"decentralized":false,"source_file":"smoke-gemini"},{"command":"smoke-github-claude","description":"Smoke test for Claude engine using GitHub provider that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-github-claude"},{"command":"smoke-goose","description":"Smoke test workflow that validates Goose engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-goose"},{"command":"smoke-kiro","description":"Smoke test workflow that validates Kiro engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-kiro"},{"command":"smoke-multi-pr","description":"Test creating multiple pull requests in a single workflow run","centralized":true,"decentralized":false,"source_file":"smoke-multi-pr"},{"command":"smoke-opencode","description":"Smoke test workflow that validates OpenCode engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-opencode"},{"command":"smoke-otel-backends","description":"Smoke test that validates OTEL span export and query access for Sentry, Grafana, and Datadog","centralized":true,"decentralized":false,"source_file":"smoke-otel-backends"},{"command":"smoke-pi","description":"Smoke test workflow that validates Pi engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pi"},{"command":"smoke-project","description":"Smoke Project - Test project operations","centralized":true,"decentralized":false,"source_file":"smoke-project"},{"command":"smoke-pydantic","description":"Smoke test workflow that validates Pydantic AI engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pydantic"},{"command":"smoke-service-ports","description":"Smoke test to validate --allow-host-service-ports with Redis service container","centralized":true,"decentralized":false,"source_file":"smoke-service-ports"},{"command":"smoke-temporary-id","description":"Test temporary ID functionality for issue chaining and cross-references","centralized":true,"decentralized":false,"source_file":"smoke-temporary-id"},{"command":"smoke-test-tools","description":"Smoke test to validate common development tools are available in the agent container","centralized":true,"decentralized":false,"source_file":"smoke-test-tools"},{"command":"smoke-update-cross-repo-pr","description":"Smoke test validating cross-repo pull request updates in github/gh-aw-side-repo by adding lines from Homer''s Odyssey to the README","centralized":true,"decentralized":false,"source_file":"smoke-update-cross-repo-pr"},{"command":"souschef","description":"Keeps open non-draft PRs moving toward maintainer investigation by posting targeted Copilot nudges","centralized":true,"decentralized":false,"source_file":"pr-sous-chef"},{"command":"squad","description":"Cast, connect, or adopt a Squad AI team for your repository","centralized":false,"decentralized":true,"source_file":"squad"},{"command":"squad-plan","description":"Uses Squad to plan an issue from the /squad-plan slash command and create Copilot-ready sub-issues","centralized":true,"decentralized":false,"source_file":"squad-plan"},{"command":"summarize","description":"pdf summarizer","centralized":true,"decentralized":false,"source_file":"pdf-summary"},{"command":"tidy","description":"Automatically formats and tidies code files (Go, JS, TypeScript) on schedule or command","centralized":true,"decentralized":false,"source_file":"tidy"},{"command":"unbloat","description":"Reviews and simplifies documentation by reducing verbosity while maintaining clarity and completeness","centralized":true,"decentralized":false,"source_file":"unbloat-docs"},{"command":"approach-proposal","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"ci-doctor","description":"Investigates failed CI workflows to identify root causes and patterns, creating issues with diagnostic information; also reviews PR check failures when the ci-doctor label is applied","centralized":false,"decentralized":false,"label":true,"source_file":"ci-doctor"},{"command":"cloclo","centralized":false,"decentralized":false,"label":true,"source_file":"cloclo"},{"command":"dev","description":"Daily status report for gh-aw project","centralized":false,"decentralized":false,"label":true,"source_file":"dev"},{"command":"necromancer","description":"Investigates merge-ready pull requests, traces root-cause issues, and adds regression tests before merge","centralized":false,"decentralized":false,"label":true,"source_file":"necromancer"},{"command":"needs-design","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"smoke","description":"Smoke Copilot - AOAI (apikey)","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-sdk","description":"Smoke Copilot SDK","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-sdk"}]' GH_AW_HELP_COMMAND_ENABLED: 'true' GH_AW_SLASH_COMMAND_DOCS_URL: 'https://github.github.com/gh-aw/reference/command-triggers/' with: diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index fd902c7129e..d155681f698 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -260,9 +260,6 @@ "discussions": { "$ref": "#/definitions/permissions-level" }, - "drives": { - "$ref": "#/definitions/permissions-level" - }, "id-token": { "$ref": "#/definitions/permissions-level" }, @@ -295,6 +292,9 @@ "type": "string", "enum": ["write", "none"] }, + "drives": { + "$ref": "#/definitions/permissions-level" + }, "vulnerability-alerts": { "type": "string", "enum": ["read", "none"] From bf66d5b777eb54b8b1870e9781bed0f1c1c6e8f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:33:29 +0000 Subject: [PATCH 8/9] Revert "Investigate CI failure" This reverts commit 9ebdde90ddad853ab7bc45b847c2dd4d8cbf2b8a. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/agentic_commands.yml | 7 +++---- pkg/workflow/schemas/github-workflow.json | 6 +++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/agentic_commands.yml b/.github/workflows/agentic_commands.yml index e9f5699b47d..dbb43b5ca6a 100644 --- a/.github/workflows/agentic_commands.yml +++ b/.github/workflows/agentic_commands.yml @@ -1,4 +1,4 @@ -# gh-aw-commands: {"payload_version":"v1","schema_version":"v1","compiler_version":"dev","commands":["*","ace","approach-validator","archie","cloclo","craft","dependabot-burner","grumpy","matt","mergefest","nit","plan","poem-bot","ponytail","review","ruflo","scout","security-review","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-drive","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","souschef","squad-plan","summarize","tidy","unbloat"],"workflows":["ace-editor","approach-validator","archie","ci-doctor","cloclo","craft","dependabot-burner","design-decision-gate","dev","grumpy-reviewer","mattpocock-skills-reviewer","mergefest","necromancer","pdf-summary","plan","poem-bot","ponytail-reviewer","pr-code-quality-reviewer","pr-nitpick-reviewer","pr-sous-chef","ruflo-backed-task","scout","security-review","skillet","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-drive","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","squad-plan","test-quality-sentinel","tidy","unbloat-docs"]} +# gh-aw-commands: {"payload_version":"v1","schema_version":"v1","compiler_version":"dev","commands":["*","ace","approach-validator","archie","cloclo","craft","dependabot-burner","grumpy","matt","mergefest","nit","plan","poem-bot","ponytail","review","ruflo","scout","security-review","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","souschef","squad-plan","summarize","tidy","unbloat"],"workflows":["ace-editor","approach-validator","archie","ci-doctor","cloclo","craft","dependabot-burner","design-decision-gate","dev","grumpy-reviewer","mattpocock-skills-reviewer","mergefest","necromancer","pdf-summary","plan","poem-bot","ponytail-reviewer","pr-code-quality-reviewer","pr-nitpick-reviewer","pr-sous-chef","ruflo-backed-task","scout","security-review","skillet","smoke-agent-all-merged","smoke-agent-all-none","smoke-agent-public-approved","smoke-agent-public-none","smoke-agent-scoped-approved","smoke-aider","smoke-call-workflow","smoke-checkout-pr-dispatch","smoke-claude","smoke-claude-on-copilot","smoke-codex","smoke-copilot","smoke-copilot-aoai-apikey","smoke-copilot-aoai-entra","smoke-copilot-arm","smoke-copilot-mai","smoke-copilot-sdk","smoke-copilot-small","smoke-create-cross-repo-pr","smoke-crush","smoke-cursor","smoke-deepseek-harness","smoke-gemini","smoke-github-claude","smoke-goose","smoke-kiro","smoke-multi-pr","smoke-opencode","smoke-otel-backends","smoke-pi","smoke-project","smoke-pydantic","smoke-service-ports","smoke-temporary-id","smoke-test-tools","smoke-update-cross-repo-pr","squad-plan","test-quality-sentinel","tidy","unbloat-docs"]} # Routing summary (sorted): # slash commands: # /* -> skillet [pull_request_comment,pull_request_review_comment] reaction=eyes @@ -43,7 +43,6 @@ # /smoke-crush -> smoke-crush [issue_comment,issues,pull_request,pull_request_comment] reaction=eyes # /smoke-cursor -> smoke-cursor [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-deepseek-harness -> smoke-deepseek-harness [issue_comment,issues,pull_request,pull_request_comment] reaction=eyes -# /smoke-drive -> smoke-drive [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-gemini -> smoke-gemini [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket # /smoke-github-claude -> smoke-github-claude [pull_request,pull_request_comment] reaction=eyes # /smoke-goose -> smoke-goose [issue_comment,issues,pull_request,pull_request_comment] reaction=rocket @@ -142,9 +141,9 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 # runner-guard:ignore RGS-016 -- routing tables below contain emoji variation selectors (U+FE0F) and zero-width joiners (U+200D) used to render standard emoji sequences, not steganographic payloads. env: - GH_AW_SLASH_ROUTING: '{"*":[{"workflow":"skillet","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🍳","status_comment":true}],"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"✏️","status_comment":true}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🏛️","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"📊","status_comment":true}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes","emoji":"✍️","status_comment":true}],"dependabot-burner":[{"workflow":"dependabot-burner","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔥","status_comment":true}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🔀","status_comment":true}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes","emoji":"📋","status_comment":true}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes","emoji":"🎭","status_comment":true}],"ponytail":[{"workflow":"ponytail-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"✂️","status_comment":true}],"review":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🏗️","status_comment":true},{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true},{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"ruflo":[{"workflow":"ruflo-backed-task","events":["issue_comment"],"ai_reaction":"eyes","status_comment":true}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔭","status_comment":true}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔒","status_comment":true}],"smoke-agent-all-merged":[{"workflow":"smoke-agent-all-merged","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-all-none":[{"workflow":"smoke-agent-all-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-approved":[{"workflow":"smoke-agent-public-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-none":[{"workflow":"smoke-agent-public-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-scoped-approved":[{"workflow":"smoke-agent-scoped-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-aider":[{"workflow":"smoke-aider","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧑‍✈️","status_comment":true}],"smoke-call-workflow":[{"workflow":"smoke-call-workflow","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-checkout-pr-dispatch":[{"workflow":"smoke-checkout-pr-dispatch","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-claude":[{"workflow":"smoke-claude","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"heart","emoji":"🧪","status_comment":true}],"smoke-claude-on-copilot":[{"workflow":"smoke-claude-on-copilot","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-codex":[{"workflow":"smoke-codex","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"hooray","emoji":"🧪","status_comment":true}],"smoke-copilot":[{"workflow":"smoke-copilot","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-apikey":[{"workflow":"smoke-copilot-aoai-apikey","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-entra":[{"workflow":"smoke-copilot-aoai-entra","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-arm":[{"workflow":"smoke-copilot-arm","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-mai":[{"workflow":"smoke-copilot-mai","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"⚡","status_comment":true}],"smoke-copilot-sdk":[{"workflow":"smoke-copilot-sdk","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🔬","status_comment":true}],"smoke-copilot-small":[{"workflow":"smoke-copilot-small","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🪶","status_comment":true}],"smoke-create-cross-repo-pr":[{"workflow":"smoke-create-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-crush":[{"workflow":"smoke-crush","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-cursor":[{"workflow":"smoke-cursor","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🖱️","status_comment":true}],"smoke-deepseek-harness":[{"workflow":"smoke-deepseek-harness","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-drive":[{"workflow":"smoke-drive","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"💾","status_comment":true}],"smoke-gemini":[{"workflow":"smoke-gemini","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-github-claude":[{"workflow":"smoke-github-claude","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-goose":[{"workflow":"smoke-goose","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🪿","status_comment":true}],"smoke-kiro":[{"workflow":"smoke-kiro","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧭","status_comment":true}],"smoke-multi-pr":[{"workflow":"smoke-multi-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-opencode":[{"workflow":"smoke-opencode","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-otel-backends":[{"workflow":"smoke-otel-backends","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pi":[{"workflow":"smoke-pi","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-project":[{"workflow":"smoke-project","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pydantic":[{"workflow":"smoke-pydantic","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🐍","status_comment":true}],"smoke-service-ports":[{"workflow":"smoke-service-ports","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-temporary-id":[{"workflow":"smoke-temporary-id","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-test-tools":[{"workflow":"smoke-test-tools","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-update-cross-repo-pr":[{"workflow":"smoke-update-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"souschef":[{"workflow":"pr-sous-chef","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"👨‍🍳","status_comment":true}],"squad-plan":[{"workflow":"squad-plan","events":["issue_comment"],"ai_reaction":"eyes","emoji":"🧑‍🤝‍🧑","status_comment":true}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes","emoji":"📄","status_comment":true}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🧹","status_comment":true}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"📝","status_comment":true}]}' + GH_AW_SLASH_ROUTING: '{"*":[{"workflow":"skillet","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🍳","status_comment":true}],"ace":[{"workflow":"ace-editor","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"✏️","status_comment":true}],"approach-validator":[{"workflow":"approach-validator","events":["issue_comment","pull_request_comment"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"archie":[{"workflow":"archie","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🏛️","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"📊","status_comment":true}],"craft":[{"workflow":"craft","events":["issues"],"ai_reaction":"eyes","emoji":"✍️","status_comment":true}],"dependabot-burner":[{"workflow":"dependabot-burner","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔥","status_comment":true}],"grumpy":[{"workflow":"grumpy-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"matt":[{"workflow":"mattpocock-skills-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"mergefest":[{"workflow":"mergefest","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🔀","status_comment":true}],"nit":[{"workflow":"pr-nitpick-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true}],"plan":[{"workflow":"plan","events":["discussion_comment","issue_comment"],"ai_reaction":"eyes","emoji":"📋","status_comment":true}],"poem-bot":[{"workflow":"poem-bot","events":["issues"],"ai_reaction":"eyes","emoji":"🎭","status_comment":true}],"ponytail":[{"workflow":"ponytail-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"✂️","status_comment":true}],"review":[{"workflow":"design-decision-gate","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🏗️","status_comment":true},{"workflow":"pr-code-quality-reviewer","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔍","status_comment":true},{"workflow":"test-quality-sentinel","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"ruflo":[{"workflow":"ruflo-backed-task","events":["issue_comment"],"ai_reaction":"eyes","status_comment":true}],"scout":[{"workflow":"scout","events":["discussion","discussion_comment","issue_comment","issues","pull_request","pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔭","status_comment":true}],"security-review":[{"workflow":"security-review","events":["pull_request_comment","pull_request_review_comment"],"ai_reaction":"eyes","emoji":"🔒","status_comment":true}],"smoke-agent-all-merged":[{"workflow":"smoke-agent-all-merged","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-all-none":[{"workflow":"smoke-agent-all-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-approved":[{"workflow":"smoke-agent-public-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-public-none":[{"workflow":"smoke-agent-public-none","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-agent-scoped-approved":[{"workflow":"smoke-agent-scoped-approved","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-aider":[{"workflow":"smoke-aider","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧑‍✈️","status_comment":true}],"smoke-call-workflow":[{"workflow":"smoke-call-workflow","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-checkout-pr-dispatch":[{"workflow":"smoke-checkout-pr-dispatch","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-claude":[{"workflow":"smoke-claude","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"heart","emoji":"🧪","status_comment":true}],"smoke-claude-on-copilot":[{"workflow":"smoke-claude-on-copilot","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-codex":[{"workflow":"smoke-codex","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"hooray","emoji":"🧪","status_comment":true}],"smoke-copilot":[{"workflow":"smoke-copilot","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-apikey":[{"workflow":"smoke-copilot-aoai-apikey","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-aoai-entra":[{"workflow":"smoke-copilot-aoai-entra","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-arm":[{"workflow":"smoke-copilot-arm","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-copilot-mai":[{"workflow":"smoke-copilot-mai","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"⚡","status_comment":true}],"smoke-copilot-sdk":[{"workflow":"smoke-copilot-sdk","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🔬","status_comment":true}],"smoke-copilot-small":[{"workflow":"smoke-copilot-small","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🪶","status_comment":true}],"smoke-create-cross-repo-pr":[{"workflow":"smoke-create-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-crush":[{"workflow":"smoke-crush","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-cursor":[{"workflow":"smoke-cursor","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🖱️","status_comment":true}],"smoke-deepseek-harness":[{"workflow":"smoke-deepseek-harness","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-gemini":[{"workflow":"smoke-gemini","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-github-claude":[{"workflow":"smoke-github-claude","events":["pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-goose":[{"workflow":"smoke-goose","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🪿","status_comment":true}],"smoke-kiro":[{"workflow":"smoke-kiro","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧭","status_comment":true}],"smoke-multi-pr":[{"workflow":"smoke-multi-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-opencode":[{"workflow":"smoke-opencode","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-otel-backends":[{"workflow":"smoke-otel-backends","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pi":[{"workflow":"smoke-pi","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🧪","status_comment":true}],"smoke-project":[{"workflow":"smoke-project","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-pydantic":[{"workflow":"smoke-pydantic","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"rocket","emoji":"🐍","status_comment":true}],"smoke-service-ports":[{"workflow":"smoke-service-ports","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-temporary-id":[{"workflow":"smoke-temporary-id","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-test-tools":[{"workflow":"smoke-test-tools","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-update-cross-repo-pr":[{"workflow":"smoke-update-cross-repo-pr","events":["issue_comment","issues","pull_request","pull_request_comment"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"souschef":[{"workflow":"pr-sous-chef","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"👨‍🍳","status_comment":true}],"squad-plan":[{"workflow":"squad-plan","events":["issue_comment"],"ai_reaction":"eyes","emoji":"🧑‍🤝‍🧑","status_comment":true}],"summarize":[{"workflow":"pdf-summary","events":["issue_comment","issues"],"ai_reaction":"eyes","emoji":"📄","status_comment":true}],"tidy":[{"workflow":"tidy","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"🧹","status_comment":true}],"unbloat":[{"workflow":"unbloat-docs","events":["pull_request_comment"],"ai_reaction":"eyes","emoji":"📝","status_comment":true}]}' GH_AW_LABEL_ROUTING: '{"approach-proposal":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"ci-doctor":[{"workflow":"ci-doctor","events":["pull_request"],"ai_reaction":"eyes","emoji":"🏥","status_comment":true}],"cloclo":[{"workflow":"cloclo","events":["discussion","issues","pull_request"],"ai_reaction":"eyes","emoji":"📊","status_comment":true}],"dev":[{"workflow":"dev","events":["discussion","issues","pull_request"],"ai_reaction":"eyes","emoji":"💻","status_comment":true}],"necromancer":[{"workflow":"necromancer","events":["pull_request"],"ai_reaction":"eyes","emoji":"💀","status_comment":true}],"needs-design":[{"workflow":"approach-validator","events":["issues","pull_request"],"ai_reaction":"eyes","emoji":"✅","status_comment":true}],"smoke":[{"workflow":"smoke-copilot","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true},{"workflow":"smoke-copilot-aoai-apikey","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true},{"workflow":"smoke-copilot-aoai-entra","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true},{"workflow":"smoke-copilot-mai","events":["pull_request"],"ai_reaction":"eyes","emoji":"⚡","status_comment":true},{"workflow":"smoke-copilot-small","events":["pull_request"],"ai_reaction":"eyes","emoji":"🪶","status_comment":true},{"workflow":"smoke-otel-backends","events":["pull_request"],"ai_reaction":"eyes","emoji":"🧪","status_comment":true}],"smoke-sdk":[{"workflow":"smoke-copilot-sdk","events":["pull_request"],"ai_reaction":"eyes","emoji":"🔬","status_comment":true}]}' - GH_AW_HELP_COMMANDS: '[{"command":"*","description":"Reviews pull requests by mapping any slash command to a matching repository skill under .github/skills","centralized":true,"decentralized":false,"source_file":"skillet"},{"command":"ace","description":"Generates an ACE editor session link when invoked with /ace command on pull request comments","centralized":true,"decentralized":false,"source_file":"ace-editor"},{"command":"approach-validator","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":true,"decentralized":false,"source_file":"approach-validator"},{"command":"archie","description":"Generates Mermaid diagrams to visualize issue and pull request relationships when invoked with the /archie command","centralized":true,"decentralized":false,"source_file":"archie"},{"command":"cloclo","centralized":true,"decentralized":false,"source_file":"cloclo"},{"command":"craft","description":"Generates new agentic workflow markdown files based on user requests when invoked with /craft command","centralized":true,"decentralized":false,"source_file":"craft"},{"command":"dependabot-burner","description":"Runs one grouped Dependabot remediation wave from schedule, manual dispatch, or /dependabot-burner on pull requests","centralized":true,"decentralized":false,"source_file":"dependabot-burner"},{"command":"grumpy","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Performs critical code review with a focus on edge cases, potential bugs, and code quality issues","centralized":true,"decentralized":false,"source_file":"grumpy-reviewer"},{"command":"matt","description":"Reviews pull requests using Matt Pocock''s engineering skills to provide targeted, high-quality improvement suggestions based on the type of changes","centralized":true,"decentralized":false,"source_file":"mattpocock-skills-reviewer"},{"command":"mergefest","description":"Automatically merges the main branch into pull request branches when invoked with /mergefest command","centralized":true,"decentralized":false,"source_file":"mergefest"},{"command":"nit","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Provides detailed nitpicky code review focusing on style, best practices, and minor improvements","centralized":true,"decentralized":false,"source_file":"pr-nitpick-reviewer"},{"command":"plan","description":"Generates project plans and task breakdowns when invoked with /plan command in issues or PRs","centralized":true,"decentralized":false,"source_file":"plan"},{"command":"poem-bot","description":"Generates creative poems on specified themes when invoked with /poem-bot command","centralized":true,"decentralized":false,"source_file":"poem-bot"},{"command":"ponytail","description":"Reviews pull requests for unnecessary complexity using Ponytail","centralized":true,"decentralized":false,"source_file":"ponytail-reviewer"},{"command":"q","description":"Intelligent assistant that answers questions, analyzes repositories, and can create PRs for workflow optimizations","centralized":false,"decentralized":true,"source_file":"q"},{"command":"review","description":"Enforces Architecture Decision Records (ADRs) before implementation work can merge, detecting missing design decisions and generating draft ADRs using AI analysis","centralized":true,"decentralized":false,"source_file":"design-decision-gate"},{"command":"ruflo","description":"Runs a repository task inside GitHub Agentic Workflows while delegating inner planning and coordination to Ruflo","centralized":true,"decentralized":false,"source_file":"ruflo-backed-task"},{"command":"scout","description":"Performs deep research investigations using web search to gather and synthesize comprehensive information on any topic","centralized":true,"decentralized":false,"source_file":"scout"},{"command":"security-review","description":"Security-focused AI agent that reviews pull requests to identify changes that could weaken security posture or extend AWF boundaries","centralized":true,"decentralized":false,"source_file":"security-review"},{"command":"smoke-agent-all-merged","description":"Guard policy smoke test: repos=all, min-integrity=merged (most restrictive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-merged"},{"command":"smoke-agent-all-none","description":"Guard policy smoke test: repos=all, min-integrity=none (most permissive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-none"},{"command":"smoke-agent-public-approved","description":"Smoke test that validates assign-to-agent with the agentic-workflows custom agent","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-approved"},{"command":"smoke-agent-public-none","description":"Guard policy smoke test: repos=public, min-integrity=none","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-none"},{"command":"smoke-agent-scoped-approved","description":"Guard policy smoke test: repos=[github/gh-aw, github/*], min-integrity=approved (scoped patterns)","centralized":true,"decentralized":false,"source_file":"smoke-agent-scoped-approved"},{"command":"smoke-aider","description":"Smoke test workflow that validates Aider engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-aider"},{"command":"smoke-call-workflow","description":"Smoke test for the call-workflow safe output - orchestrator that calls a worker via workflow_call at compile-time fan-out","centralized":true,"decentralized":false,"source_file":"smoke-call-workflow"},{"command":"smoke-checkout-pr-dispatch","description":"Integration test validating that workflow_dispatch events with aw_context.item_type == ''pull_request'' correctly check out the PR branch","centralized":true,"decentralized":false,"source_file":"smoke-checkout-pr-dispatch"},{"command":"smoke-claude","description":"Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-claude"},{"command":"smoke-claude-on-copilot","description":"Smoke test for Claude engine on GitHub Inference that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-claude-on-copilot"},{"command":"smoke-codex","description":"Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-codex"},{"command":"smoke-copilot","description":"Smoke Copilot","centralized":true,"decentralized":false,"source_file":"smoke-copilot"},{"command":"smoke-copilot-aoai-apikey","description":"Smoke Copilot - AOAI (apikey)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-copilot-aoai-entra","description":"Smoke Copilot - AOAI (Entra)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-entra"},{"command":"smoke-copilot-arm","description":"Smoke Copilot ARM64","centralized":true,"decentralized":false,"source_file":"smoke-copilot-arm"},{"command":"smoke-copilot-mai","description":"Smoke test for MAI-Code-1-Flash (mai-code-1-flash-picker) — pricing: $0.75/M input, $0.075/M cached, $4.50/M output","centralized":true,"decentralized":false,"source_file":"smoke-copilot-mai"},{"command":"smoke-copilot-sdk","description":"Smoke Copilot SDK","centralized":true,"decentralized":false,"source_file":"smoke-copilot-sdk"},{"command":"smoke-copilot-small","description":"Smoke Copilot Small","centralized":true,"decentralized":false,"source_file":"smoke-copilot-small"},{"command":"smoke-create-cross-repo-pr","description":"Smoke test validating cross-repo pull request creation in github/gh-aw-side-repo","centralized":true,"decentralized":false,"source_file":"smoke-create-cross-repo-pr"},{"command":"smoke-crush","description":"Smoke test workflow that validates Crush engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-crush"},{"command":"smoke-cursor","description":"Smoke test workflow that validates Cursor engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-cursor"},{"command":"smoke-deepseek-harness","description":"Smoke test workflow that validates DeepSeek Harness engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-deepseek-harness"},{"command":"smoke-drive","description":"Smoke test workflow that validates experimental GitHub Drives memory","centralized":true,"decentralized":false,"source_file":"smoke-drive"},{"command":"smoke-gemini","description":"Smoke test workflow that validates Gemini engine functionality twice daily","centralized":true,"decentralized":false,"source_file":"smoke-gemini"},{"command":"smoke-github-claude","description":"Smoke test for Claude engine using GitHub provider that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-github-claude"},{"command":"smoke-goose","description":"Smoke test workflow that validates Goose engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-goose"},{"command":"smoke-kiro","description":"Smoke test workflow that validates Kiro engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-kiro"},{"command":"smoke-multi-pr","description":"Test creating multiple pull requests in a single workflow run","centralized":true,"decentralized":false,"source_file":"smoke-multi-pr"},{"command":"smoke-opencode","description":"Smoke test workflow that validates OpenCode engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-opencode"},{"command":"smoke-otel-backends","description":"Smoke test that validates OTEL span export and query access for Sentry, Grafana, and Datadog","centralized":true,"decentralized":false,"source_file":"smoke-otel-backends"},{"command":"smoke-pi","description":"Smoke test workflow that validates Pi engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pi"},{"command":"smoke-project","description":"Smoke Project - Test project operations","centralized":true,"decentralized":false,"source_file":"smoke-project"},{"command":"smoke-pydantic","description":"Smoke test workflow that validates Pydantic AI engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pydantic"},{"command":"smoke-service-ports","description":"Smoke test to validate --allow-host-service-ports with Redis service container","centralized":true,"decentralized":false,"source_file":"smoke-service-ports"},{"command":"smoke-temporary-id","description":"Test temporary ID functionality for issue chaining and cross-references","centralized":true,"decentralized":false,"source_file":"smoke-temporary-id"},{"command":"smoke-test-tools","description":"Smoke test to validate common development tools are available in the agent container","centralized":true,"decentralized":false,"source_file":"smoke-test-tools"},{"command":"smoke-update-cross-repo-pr","description":"Smoke test validating cross-repo pull request updates in github/gh-aw-side-repo by adding lines from Homer''s Odyssey to the README","centralized":true,"decentralized":false,"source_file":"smoke-update-cross-repo-pr"},{"command":"souschef","description":"Keeps open non-draft PRs moving toward maintainer investigation by posting targeted Copilot nudges","centralized":true,"decentralized":false,"source_file":"pr-sous-chef"},{"command":"squad","description":"Cast, connect, or adopt a Squad AI team for your repository","centralized":false,"decentralized":true,"source_file":"squad"},{"command":"squad-plan","description":"Uses Squad to plan an issue from the /squad-plan slash command and create Copilot-ready sub-issues","centralized":true,"decentralized":false,"source_file":"squad-plan"},{"command":"summarize","description":"pdf summarizer","centralized":true,"decentralized":false,"source_file":"pdf-summary"},{"command":"tidy","description":"Automatically formats and tidies code files (Go, JS, TypeScript) on schedule or command","centralized":true,"decentralized":false,"source_file":"tidy"},{"command":"unbloat","description":"Reviews and simplifies documentation by reducing verbosity while maintaining clarity and completeness","centralized":true,"decentralized":false,"source_file":"unbloat-docs"},{"command":"approach-proposal","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"ci-doctor","description":"Investigates failed CI workflows to identify root causes and patterns, creating issues with diagnostic information; also reviews PR check failures when the ci-doctor label is applied","centralized":false,"decentralized":false,"label":true,"source_file":"ci-doctor"},{"command":"cloclo","centralized":false,"decentralized":false,"label":true,"source_file":"cloclo"},{"command":"dev","description":"Daily status report for gh-aw project","centralized":false,"decentralized":false,"label":true,"source_file":"dev"},{"command":"necromancer","description":"Investigates merge-ready pull requests, traces root-cause issues, and adds regression tests before merge","centralized":false,"decentralized":false,"label":true,"source_file":"necromancer"},{"command":"needs-design","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"smoke","description":"Smoke Copilot - AOAI (apikey)","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-sdk","description":"Smoke Copilot SDK","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-sdk"}]' + GH_AW_HELP_COMMANDS: '[{"command":"*","description":"Reviews pull requests by mapping any slash command to a matching repository skill under .github/skills","centralized":true,"decentralized":false,"source_file":"skillet"},{"command":"ace","description":"Generates an ACE editor session link when invoked with /ace command on pull request comments","centralized":true,"decentralized":false,"source_file":"ace-editor"},{"command":"approach-validator","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":true,"decentralized":false,"source_file":"approach-validator"},{"command":"archie","description":"Generates Mermaid diagrams to visualize issue and pull request relationships when invoked with the /archie command","centralized":true,"decentralized":false,"source_file":"archie"},{"command":"cloclo","centralized":true,"decentralized":false,"source_file":"cloclo"},{"command":"craft","description":"Generates new agentic workflow markdown files based on user requests when invoked with /craft command","centralized":true,"decentralized":false,"source_file":"craft"},{"command":"dependabot-burner","description":"Runs one grouped Dependabot remediation wave from schedule, manual dispatch, or /dependabot-burner on pull requests","centralized":true,"decentralized":false,"source_file":"dependabot-burner"},{"command":"grumpy","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Performs critical code review with a focus on edge cases, potential bugs, and code quality issues","centralized":true,"decentralized":false,"source_file":"grumpy-reviewer"},{"command":"matt","description":"Reviews pull requests using Matt Pocock''s engineering skills to provide targeted, high-quality improvement suggestions based on the type of changes","centralized":true,"decentralized":false,"source_file":"mattpocock-skills-reviewer"},{"command":"mergefest","description":"Automatically merges the main branch into pull request branches when invoked with /mergefest command","centralized":true,"decentralized":false,"source_file":"mergefest"},{"command":"nit","description":"⚠️ DEPRECATED: Use PR Code Quality Reviewer (pr-code-quality-reviewer) instead. Provides detailed nitpicky code review focusing on style, best practices, and minor improvements","centralized":true,"decentralized":false,"source_file":"pr-nitpick-reviewer"},{"command":"plan","description":"Generates project plans and task breakdowns when invoked with /plan command in issues or PRs","centralized":true,"decentralized":false,"source_file":"plan"},{"command":"poem-bot","description":"Generates creative poems on specified themes when invoked with /poem-bot command","centralized":true,"decentralized":false,"source_file":"poem-bot"},{"command":"ponytail","description":"Reviews pull requests for unnecessary complexity using Ponytail","centralized":true,"decentralized":false,"source_file":"ponytail-reviewer"},{"command":"q","description":"Intelligent assistant that answers questions, analyzes repositories, and can create PRs for workflow optimizations","centralized":false,"decentralized":true,"source_file":"q"},{"command":"review","description":"Enforces Architecture Decision Records (ADRs) before implementation work can merge, detecting missing design decisions and generating draft ADRs using AI analysis","centralized":true,"decentralized":false,"source_file":"design-decision-gate"},{"command":"ruflo","description":"Runs a repository task inside GitHub Agentic Workflows while delegating inner planning and coordination to Ruflo","centralized":true,"decentralized":false,"source_file":"ruflo-backed-task"},{"command":"scout","description":"Performs deep research investigations using web search to gather and synthesize comprehensive information on any topic","centralized":true,"decentralized":false,"source_file":"scout"},{"command":"security-review","description":"Security-focused AI agent that reviews pull requests to identify changes that could weaken security posture or extend AWF boundaries","centralized":true,"decentralized":false,"source_file":"security-review"},{"command":"smoke-agent-all-merged","description":"Guard policy smoke test: repos=all, min-integrity=merged (most restrictive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-merged"},{"command":"smoke-agent-all-none","description":"Guard policy smoke test: repos=all, min-integrity=none (most permissive)","centralized":true,"decentralized":false,"source_file":"smoke-agent-all-none"},{"command":"smoke-agent-public-approved","description":"Smoke test that validates assign-to-agent with the agentic-workflows custom agent","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-approved"},{"command":"smoke-agent-public-none","description":"Guard policy smoke test: repos=public, min-integrity=none","centralized":true,"decentralized":false,"source_file":"smoke-agent-public-none"},{"command":"smoke-agent-scoped-approved","description":"Guard policy smoke test: repos=[github/gh-aw, github/*], min-integrity=approved (scoped patterns)","centralized":true,"decentralized":false,"source_file":"smoke-agent-scoped-approved"},{"command":"smoke-aider","description":"Smoke test workflow that validates Aider engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-aider"},{"command":"smoke-call-workflow","description":"Smoke test for the call-workflow safe output - orchestrator that calls a worker via workflow_call at compile-time fan-out","centralized":true,"decentralized":false,"source_file":"smoke-call-workflow"},{"command":"smoke-checkout-pr-dispatch","description":"Integration test validating that workflow_dispatch events with aw_context.item_type == ''pull_request'' correctly check out the PR branch","centralized":true,"decentralized":false,"source_file":"smoke-checkout-pr-dispatch"},{"command":"smoke-claude","description":"Smoke test workflow that validates Claude engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-claude"},{"command":"smoke-claude-on-copilot","description":"Smoke test for Claude engine on GitHub Inference that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-claude-on-copilot"},{"command":"smoke-codex","description":"Smoke test workflow that validates Codex engine functionality by reviewing recent PRs twice daily","centralized":true,"decentralized":false,"source_file":"smoke-codex"},{"command":"smoke-copilot","description":"Smoke Copilot","centralized":true,"decentralized":false,"source_file":"smoke-copilot"},{"command":"smoke-copilot-aoai-apikey","description":"Smoke Copilot - AOAI (apikey)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-copilot-aoai-entra","description":"Smoke Copilot - AOAI (Entra)","centralized":true,"decentralized":false,"source_file":"smoke-copilot-aoai-entra"},{"command":"smoke-copilot-arm","description":"Smoke Copilot ARM64","centralized":true,"decentralized":false,"source_file":"smoke-copilot-arm"},{"command":"smoke-copilot-mai","description":"Smoke test for MAI-Code-1-Flash (mai-code-1-flash-picker) — pricing: $0.75/M input, $0.075/M cached, $4.50/M output","centralized":true,"decentralized":false,"source_file":"smoke-copilot-mai"},{"command":"smoke-copilot-sdk","description":"Smoke Copilot SDK","centralized":true,"decentralized":false,"source_file":"smoke-copilot-sdk"},{"command":"smoke-copilot-small","description":"Smoke Copilot Small","centralized":true,"decentralized":false,"source_file":"smoke-copilot-small"},{"command":"smoke-create-cross-repo-pr","description":"Smoke test validating cross-repo pull request creation in github/gh-aw-side-repo","centralized":true,"decentralized":false,"source_file":"smoke-create-cross-repo-pr"},{"command":"smoke-crush","description":"Smoke test workflow that validates Crush engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-crush"},{"command":"smoke-cursor","description":"Smoke test workflow that validates Cursor engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-cursor"},{"command":"smoke-deepseek-harness","description":"Smoke test workflow that validates DeepSeek Harness engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-deepseek-harness"},{"command":"smoke-gemini","description":"Smoke test workflow that validates Gemini engine functionality twice daily","centralized":true,"decentralized":false,"source_file":"smoke-gemini"},{"command":"smoke-github-claude","description":"Smoke test for Claude engine using GitHub provider that posts a concise PR summary comment","centralized":true,"decentralized":false,"source_file":"smoke-github-claude"},{"command":"smoke-goose","description":"Smoke test workflow that validates Goose engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-goose"},{"command":"smoke-kiro","description":"Smoke test workflow that validates Kiro engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-kiro"},{"command":"smoke-multi-pr","description":"Test creating multiple pull requests in a single workflow run","centralized":true,"decentralized":false,"source_file":"smoke-multi-pr"},{"command":"smoke-opencode","description":"Smoke test workflow that validates OpenCode engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-opencode"},{"command":"smoke-otel-backends","description":"Smoke test that validates OTEL span export and query access for Sentry, Grafana, and Datadog","centralized":true,"decentralized":false,"source_file":"smoke-otel-backends"},{"command":"smoke-pi","description":"Smoke test workflow that validates Pi engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pi"},{"command":"smoke-project","description":"Smoke Project - Test project operations","centralized":true,"decentralized":false,"source_file":"smoke-project"},{"command":"smoke-pydantic","description":"Smoke test workflow that validates Pydantic AI engine functionality","centralized":true,"decentralized":false,"source_file":"smoke-pydantic"},{"command":"smoke-service-ports","description":"Smoke test to validate --allow-host-service-ports with Redis service container","centralized":true,"decentralized":false,"source_file":"smoke-service-ports"},{"command":"smoke-temporary-id","description":"Test temporary ID functionality for issue chaining and cross-references","centralized":true,"decentralized":false,"source_file":"smoke-temporary-id"},{"command":"smoke-test-tools","description":"Smoke test to validate common development tools are available in the agent container","centralized":true,"decentralized":false,"source_file":"smoke-test-tools"},{"command":"smoke-update-cross-repo-pr","description":"Smoke test validating cross-repo pull request updates in github/gh-aw-side-repo by adding lines from Homer''s Odyssey to the README","centralized":true,"decentralized":false,"source_file":"smoke-update-cross-repo-pr"},{"command":"souschef","description":"Keeps open non-draft PRs moving toward maintainer investigation by posting targeted Copilot nudges","centralized":true,"decentralized":false,"source_file":"pr-sous-chef"},{"command":"squad","description":"Cast, connect, or adopt a Squad AI team for your repository","centralized":false,"decentralized":true,"source_file":"squad"},{"command":"squad-plan","description":"Uses Squad to plan an issue from the /squad-plan slash command and create Copilot-ready sub-issues","centralized":true,"decentralized":false,"source_file":"squad-plan"},{"command":"summarize","description":"pdf summarizer","centralized":true,"decentralized":false,"source_file":"pdf-summary"},{"command":"tidy","description":"Automatically formats and tidies code files (Go, JS, TypeScript) on schedule or command","centralized":true,"decentralized":false,"source_file":"tidy"},{"command":"unbloat","description":"Reviews and simplifies documentation by reducing verbosity while maintaining clarity and completeness","centralized":true,"decentralized":false,"source_file":"unbloat-docs"},{"command":"approach-proposal","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"ci-doctor","description":"Investigates failed CI workflows to identify root causes and patterns, creating issues with diagnostic information; also reviews PR check failures when the ci-doctor label is applied","centralized":false,"decentralized":false,"label":true,"source_file":"ci-doctor"},{"command":"cloclo","centralized":false,"decentralized":false,"label":true,"source_file":"cloclo"},{"command":"dev","description":"Daily status report for gh-aw project","centralized":false,"decentralized":false,"label":true,"source_file":"dev"},{"command":"necromancer","description":"Investigates merge-ready pull requests, traces root-cause issues, and adds regression tests before merge","centralized":false,"decentralized":false,"label":true,"source_file":"necromancer"},{"command":"needs-design","description":"Validates proposed technical approaches before implementation begins using a sequential multi-agent panel of Devil''s Advocate, Alternatives Scout, Implementation Estimator, and Dead End Detector","centralized":false,"decentralized":false,"label":true,"source_file":"approach-validator"},{"command":"smoke","description":"Smoke Copilot - AOAI (apikey)","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-aoai-apikey"},{"command":"smoke-sdk","description":"Smoke Copilot SDK","centralized":false,"decentralized":false,"label":true,"source_file":"smoke-copilot-sdk"}]' GH_AW_HELP_COMMAND_ENABLED: 'true' GH_AW_SLASH_COMMAND_DOCS_URL: 'https://github.github.com/gh-aw/reference/command-triggers/' with: diff --git a/pkg/workflow/schemas/github-workflow.json b/pkg/workflow/schemas/github-workflow.json index d155681f698..fd902c7129e 100644 --- a/pkg/workflow/schemas/github-workflow.json +++ b/pkg/workflow/schemas/github-workflow.json @@ -260,6 +260,9 @@ "discussions": { "$ref": "#/definitions/permissions-level" }, + "drives": { + "$ref": "#/definitions/permissions-level" + }, "id-token": { "$ref": "#/definitions/permissions-level" }, @@ -292,9 +295,6 @@ "type": "string", "enum": ["write", "none"] }, - "drives": { - "$ref": "#/definitions/permissions-level" - }, "vulnerability-alerts": { "type": "string", "enum": ["read", "none"] From e8ce4b49e63075dd52722f83e497d98223deb363 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 15:38:53 +0000 Subject: [PATCH 9/9] Avoid closed coverage pipe in CGO tests Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/cgo.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cgo.yml b/.github/workflows/cgo.yml index 6777491ab5d..c169d7eec7c 100644 --- a/.github/workflows/cgo.yml +++ b/.github/workflows/cgo.yml @@ -265,13 +265,11 @@ jobs: run: | set -o pipefail # Run tests with JSON output for artifacts, but also show failures - go test -v -parallel=8 -timeout=3m -run='${{ matrix.pattern }}' -tags '!integration' -coverprofile=coverage-${{ matrix.shard }}.out -json ./... | tee test-result-unit-${{ matrix.shard }}.json - - # Check if tests failed by looking at JSON output - if grep -q '"Action":"fail"' test-result-unit-${{ matrix.shard }}.json; then - echo "❌ Tests failed - see output above" + if ! go test -v -parallel=8 -timeout=3m -run='${{ matrix.pattern }}' -tags '!integration' -coverprofile=coverage-${{ matrix.shard }}.out -json ./... > test-result-unit-${{ matrix.shard }}.json; then + cat test-result-unit-${{ matrix.shard }}.json exit 1 fi + cat test-result-unit-${{ matrix.shard }}.json # Generate coverage HTML report go tool cover -html=coverage-${{ matrix.shard }}.out -o coverage-${{ matrix.shard }}.html