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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 0 additions & 107 deletions pkg/cli/copilot_agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (

"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/workflow"
)

var copilotCodingAgentLog = logger.New("cli:copilot_agent")
Expand Down Expand Up @@ -179,109 +178,3 @@ func readLogHeader(path string, maxBytes int) (string, error) {

return string(buffer[:n]), nil
}

// ParseCopilotCodingAgentLogMetrics extracts metrics from GitHub Copilot coding agent logs
// This is different from Copilot CLI logs and requires specialized parsing
func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow.LogMetrics {
copilotCodingAgentLog.Printf("Parsing GitHub Copilot coding agent log metrics: %d bytes", len(logContent))

var metrics workflow.LogMetrics
var maxTokenUsage int

lines := strings.Split(logContent, "\n")
toolCallMap := make(map[string]*workflow.ToolCallInfo)
var currentSequence []string
turns := 0

// GitHub Copilot coding agent log patterns
// These patterns are designed to match the specific log format of the agent
turnPattern := regexp.MustCompile(`(?i)task.*iteration|agent.*turn|step.*\d+`)
toolCallPattern := regexp.MustCompile(`(?i)tool.*call|executing.*tool|calling.*(\w+)`)

for _, line := range lines {
// Skip empty lines
if strings.TrimSpace(line) == "" {
continue
}

// Count turns based on agent iteration patterns
if turnPattern.MatchString(line) {
turns++
// Start of a new turn, save previous sequence if any
if len(currentSequence) > 0 {
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
currentSequence = []string{}
}
}

// Extract tool calls from agent logs
if matches := toolCallPattern.FindStringSubmatch(line); len(matches) > 1 {
toolName := extractToolName(line)
if toolName != "" {
// Track tool call
if _, exists := toolCallMap[toolName]; !exists {
toolCallMap[toolName] = &workflow.ToolCallInfo{
Name: toolName,
CallCount: 0,
}
}
toolCallMap[toolName].CallCount++

// Add to current sequence
currentSequence = append(currentSequence, toolName)

if verbose {
copilotCodingAgentLog.Printf("Found tool call: %s", toolName)
}
}
}

// Try to extract token usage from JSON format if available
jsonMetrics := workflow.ExtractJSONMetrics(line, verbose)
if jsonMetrics.TokenUsage > 0 || jsonMetrics.EstimatedCost > 0 {
if jsonMetrics.TokenUsage > maxTokenUsage {
maxTokenUsage = jsonMetrics.TokenUsage
}
if jsonMetrics.EstimatedCost > 0 {
metrics.EstimatedCost += jsonMetrics.EstimatedCost
}
}
}

// Add final sequence if any
if len(currentSequence) > 0 {
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
}

// Convert tool call map to slice
for _, toolInfo := range toolCallMap {
metrics.ToolCalls = append(metrics.ToolCalls, *toolInfo)
}

metrics.TokenUsage = maxTokenUsage
metrics.Turns = turns

copilotCodingAgentLog.Printf("Parsed metrics: tokens=%d, cost=$%.4f, turns=%d",
metrics.TokenUsage, metrics.EstimatedCost, metrics.Turns)

return metrics
}

// extractToolName extracts a tool name from a log line
func extractToolName(line string) string {
// Try to extract tool name from various patterns
patterns := []*regexp.Regexp{
regexp.MustCompile(`(?i)tool[:\s]+([a-zA-Z0-9_-]+)`),
regexp.MustCompile(`(?i)calling[:\s]+([a-zA-Z0-9_-]+)`),
regexp.MustCompile(`(?i)executing[:\s]+([a-zA-Z0-9_-]+)`),
regexp.MustCompile(`(?i)using[:\s]+tool[:\s]+([a-zA-Z0-9_-]+)`),
}

for _, pattern := range patterns {
if matches := pattern.FindStringSubmatch(line); len(matches) > 1 {
return strings.TrimSpace(matches[1])
}
}

return ""
}
114 changes: 114 additions & 0 deletions pkg/cli/copilot_agent_logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cli

import (
"regexp"
"strings"

"github.com/github/gh-aw/pkg/workflow"
)

// ParseCopilotCodingAgentLogMetrics extracts metrics from GitHub Copilot coding agent logs
// This is different from Copilot CLI logs and requires specialized parsing
func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow.LogMetrics {
copilotCodingAgentLog.Printf("Parsing GitHub Copilot coding agent log metrics: %d bytes", len(logContent))

var metrics workflow.LogMetrics
var maxTokenUsage int

lines := strings.Split(logContent, "\n")
toolCallMap := make(map[string]*workflow.ToolCallInfo)
var currentSequence []string
turns := 0

// GitHub Copilot coding agent log patterns
// These patterns are designed to match the specific log format of the agent
turnPattern := regexp.MustCompile(`(?i)task.*iteration|agent.*turn|step.*\d+`)
toolCallPattern := regexp.MustCompile(`(?i)tool.*call|executing.*tool|calling.*(\w+)`)
Comment on lines +23 to +26
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

turnPattern and toolCallPattern are compiled on every call to ParseCopilotCodingAgentLogMetrics. Consider moving these regexes to package-level vars (or initializing once) to avoid repeated compilation overhead when parsing many logs.

This issue also appears on line 97 of the same file.

Copilot uses AI. Check for mistakes.

for _, line := range lines {
// Skip empty lines
if strings.TrimSpace(line) == "" {
continue
}

// Count turns based on agent iteration patterns
if turnPattern.MatchString(line) {
turns++
// Start of a new turn, save previous sequence if any
if len(currentSequence) > 0 {
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
currentSequence = []string{}
}
}

// Extract tool calls from agent logs
if matches := toolCallPattern.FindStringSubmatch(line); len(matches) > 1 {
toolName := extractToolName(line)
if toolName != "" {
// Track tool call
if _, exists := toolCallMap[toolName]; !exists {
toolCallMap[toolName] = &workflow.ToolCallInfo{
Name: toolName,
CallCount: 0,
}
}
toolCallMap[toolName].CallCount++

// Add to current sequence
currentSequence = append(currentSequence, toolName)

if verbose {
copilotCodingAgentLog.Printf("Found tool call: %s", toolName)
}
}
}

// Try to extract token usage from JSON format if available
jsonMetrics := workflow.ExtractJSONMetrics(line, verbose)
if jsonMetrics.TokenUsage > 0 || jsonMetrics.EstimatedCost > 0 {
if jsonMetrics.TokenUsage > maxTokenUsage {
maxTokenUsage = jsonMetrics.TokenUsage
}
if jsonMetrics.EstimatedCost > 0 {
metrics.EstimatedCost += jsonMetrics.EstimatedCost
}
}
}

// Add final sequence if any
if len(currentSequence) > 0 {
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
}

// Convert tool call map to slice
for _, toolInfo := range toolCallMap {
metrics.ToolCalls = append(metrics.ToolCalls, *toolInfo)
}

metrics.TokenUsage = maxTokenUsage
metrics.Turns = turns

copilotCodingAgentLog.Printf("Parsed metrics: tokens=%d, cost=$%.4f, turns=%d",
metrics.TokenUsage, metrics.EstimatedCost, metrics.Turns)

return metrics
}

// extractToolName extracts a tool name from a log line
func extractToolName(line string) string {
// Try to extract tool name from various patterns
patterns := []*regexp.Regexp{
regexp.MustCompile(`(?i)tool[:\s]+([a-zA-Z0-9_-]+)`),
regexp.MustCompile(`(?i)calling[:\s]+([a-zA-Z0-9_-]+)`),
regexp.MustCompile(`(?i)executing[:\s]+([a-zA-Z0-9_-]+)`),
regexp.MustCompile(`(?i)using[:\s]+tool[:\s]+([a-zA-Z0-9_-]+)`),
}

for _, pattern := range patterns {
if matches := pattern.FindStringSubmatch(line); len(matches) > 1 {
return strings.TrimSpace(matches[1])
}
}

return ""
}
24 changes: 1 addition & 23 deletions pkg/cli/update_extension_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"os"
"strings"

"github.com/cli/go-gh/v2/pkg/api"
"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/workflow"
Expand Down Expand Up @@ -35,7 +34,7 @@ func ensureLatestExtensionVersion(verbose bool) error {
}

// Query GitHub API for latest release
latestVersion, err := getLatestReleaseVersion(verbose)
latestVersion, err := getLatestRelease()
if err != nil {
// Fail silently - don't block upgrade if we can't check for updates
updateExtensionCheckLog.Printf("Failed to check for updates (silently ignoring): %v", err)
Expand Down Expand Up @@ -83,24 +82,3 @@ func ensureLatestExtensionVersion(verbose bool) error {

return nil
}

// getLatestReleaseVersion queries GitHub API for the latest release version of gh-aw
func getLatestReleaseVersion(verbose bool) (string, error) {
updateExtensionCheckLog.Print("Querying GitHub API for latest release...")

// Create GitHub REST client using go-gh
client, err := api.NewRESTClient(api.ClientOptions{})
if err != nil {
return "", fmt.Errorf("failed to create GitHub client: %w", err)
}

// Query the latest release
var release Release
err = client.Get("repos/github/gh-aw/releases/latest", &release)
if err != nil {
return "", fmt.Errorf("failed to query latest release: %w", err)
}

updateExtensionCheckLog.Printf("Latest release: %s", release.TagName)
return release.TagName, nil
}
85 changes: 0 additions & 85 deletions pkg/workflow/mcp_config_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,88 +288,3 @@ func renderAgenticWorkflowsMCPConfigWithOptions(yaml *strings.Builder, isLast bo
yaml.WriteString(" },\n")
}
}

// renderSafeOutputsMCPConfigTOML generates the Safe Outputs MCP server configuration in TOML format for Codex
// Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized.
// Uses MCP Gateway spec format: container, entrypoint, entrypointArgs, and mounts fields.
func renderSafeOutputsMCPConfigTOML(yaml *strings.Builder) {
yaml.WriteString(" \n")
yaml.WriteString(" [mcp_servers." + constants.SafeOutputsMCPServerID.String() + "]\n")
yaml.WriteString(" type = \"http\"\n")
yaml.WriteString(" url = \"http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT\"\n")
yaml.WriteString(" \n")
yaml.WriteString(" [mcp_servers." + constants.SafeOutputsMCPServerID.String() + ".headers]\n")
yaml.WriteString(" Authorization = \"$GH_AW_SAFE_OUTPUTS_API_KEY\"\n")
}

// renderAgenticWorkflowsMCPConfigTOML generates the Agentic Workflows MCP server configuration in TOML format for Codex
// Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized.
// Uses MCP Gateway spec format: container, entrypoint, entrypointArgs, and mounts fields.
func renderAgenticWorkflowsMCPConfigTOML(yaml *strings.Builder, actionMode ActionMode) {
yaml.WriteString(" \n")
yaml.WriteString(" [mcp_servers." + constants.AgenticWorkflowsMCPServerID.String() + "]\n")

containerImage := constants.DefaultAlpineImage
var entrypoint string
var entrypointArgs []string
var mounts []string

if actionMode.IsDev() {
// Dev mode: Use locally built Docker image which includes gh-aw binary and gh CLI
// The Dockerfile sets ENTRYPOINT ["gh-aw"] and CMD ["mcp-server"]
// So we don't need to specify entrypoint or entrypointArgs
containerImage = constants.DevModeGhAwImage
entrypoint = "" // Use container's default ENTRYPOINT
entrypointArgs = nil // Use container's default CMD
// Only mount workspace and temp directory - binary and gh CLI are in the image
mounts = []string{constants.DefaultWorkspaceMount, constants.DefaultTmpGhAwMount}
} else {
// Release mode: Use minimal Alpine image with mounted binaries
// Pass --validate-actor flag to enable role-based access control
entrypoint = "/opt/gh-aw/gh-aw"
entrypointArgs = []string{"mcp-server", "--validate-actor"}
// Mount gh-aw binary, gh CLI binary, workspace, and temp directory
mounts = []string{constants.DefaultGhAwMount, constants.DefaultGhBinaryMount, constants.DefaultWorkspaceMount, constants.DefaultTmpGhAwMount}
}

yaml.WriteString(" container = \"" + containerImage + "\"\n")

// Only write entrypoint if it's specified (release mode)
// In dev mode, use the container's default ENTRYPOINT
if entrypoint != "" {
yaml.WriteString(" entrypoint = \"" + entrypoint + "\"\n")
}

// Only write entrypointArgs if specified (release mode)
// In dev mode, use the container's default CMD
if entrypointArgs != nil {
yaml.WriteString(" entrypointArgs = [")
for i, arg := range entrypointArgs {
if i > 0 {
yaml.WriteString(", ")
}
yaml.WriteString("\"" + arg + "\"")
}
yaml.WriteString("]\n")
}

// Write mounts
yaml.WriteString(" mounts = [")
for i, mount := range mounts {
if i > 0 {
yaml.WriteString(", ")
}
yaml.WriteString("\"" + mount + "\"")
}
yaml.WriteString("]\n")

// Add Docker runtime args:
// - --network host: Enables network access for GitHub API calls (gh CLI needs api.github.com)
// - -w: Sets working directory to workspace for .github/workflows folder resolution
// Security: Use GITHUB_WORKSPACE environment variable instead of template expansion to prevent template injection
yaml.WriteString(" args = [\"--network\", \"host\", \"-w\", \"${GITHUB_WORKSPACE}\"]\n")

// Use env_vars array to reference environment variables instead of embedding secrets
// Include GITHUB_ACTOR for role-based access control and GITHUB_REPOSITORY for repository context
yaml.WriteString(" env_vars = [\"DEBUG\", \"GITHUB_TOKEN\", \"GITHUB_ACTOR\", \"GITHUB_REPOSITORY\"]\n")
}
Loading
Loading