Skip to content

Commit b00407b

Browse files
authored
refactor: resolve 4 semantic function clustering findings (#19799)
1 parent 74480df commit b00407b

File tree

9 files changed

+273
-334
lines changed

9 files changed

+273
-334
lines changed

pkg/cli/copilot_agent.go

Lines changed: 0 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
"github.com/github/gh-aw/pkg/console"
1111
"github.com/github/gh-aw/pkg/logger"
12-
"github.com/github/gh-aw/pkg/workflow"
1312
)
1413

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

180179
return string(buffer[:n]), nil
181180
}
182-
183-
// ParseCopilotCodingAgentLogMetrics extracts metrics from GitHub Copilot coding agent logs
184-
// This is different from Copilot CLI logs and requires specialized parsing
185-
func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow.LogMetrics {
186-
copilotCodingAgentLog.Printf("Parsing GitHub Copilot coding agent log metrics: %d bytes", len(logContent))
187-
188-
var metrics workflow.LogMetrics
189-
var maxTokenUsage int
190-
191-
lines := strings.Split(logContent, "\n")
192-
toolCallMap := make(map[string]*workflow.ToolCallInfo)
193-
var currentSequence []string
194-
turns := 0
195-
196-
// GitHub Copilot coding agent log patterns
197-
// These patterns are designed to match the specific log format of the agent
198-
turnPattern := regexp.MustCompile(`(?i)task.*iteration|agent.*turn|step.*\d+`)
199-
toolCallPattern := regexp.MustCompile(`(?i)tool.*call|executing.*tool|calling.*(\w+)`)
200-
201-
for _, line := range lines {
202-
// Skip empty lines
203-
if strings.TrimSpace(line) == "" {
204-
continue
205-
}
206-
207-
// Count turns based on agent iteration patterns
208-
if turnPattern.MatchString(line) {
209-
turns++
210-
// Start of a new turn, save previous sequence if any
211-
if len(currentSequence) > 0 {
212-
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
213-
currentSequence = []string{}
214-
}
215-
}
216-
217-
// Extract tool calls from agent logs
218-
if matches := toolCallPattern.FindStringSubmatch(line); len(matches) > 1 {
219-
toolName := extractToolName(line)
220-
if toolName != "" {
221-
// Track tool call
222-
if _, exists := toolCallMap[toolName]; !exists {
223-
toolCallMap[toolName] = &workflow.ToolCallInfo{
224-
Name: toolName,
225-
CallCount: 0,
226-
}
227-
}
228-
toolCallMap[toolName].CallCount++
229-
230-
// Add to current sequence
231-
currentSequence = append(currentSequence, toolName)
232-
233-
if verbose {
234-
copilotCodingAgentLog.Printf("Found tool call: %s", toolName)
235-
}
236-
}
237-
}
238-
239-
// Try to extract token usage from JSON format if available
240-
jsonMetrics := workflow.ExtractJSONMetrics(line, verbose)
241-
if jsonMetrics.TokenUsage > 0 || jsonMetrics.EstimatedCost > 0 {
242-
if jsonMetrics.TokenUsage > maxTokenUsage {
243-
maxTokenUsage = jsonMetrics.TokenUsage
244-
}
245-
if jsonMetrics.EstimatedCost > 0 {
246-
metrics.EstimatedCost += jsonMetrics.EstimatedCost
247-
}
248-
}
249-
}
250-
251-
// Add final sequence if any
252-
if len(currentSequence) > 0 {
253-
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
254-
}
255-
256-
// Convert tool call map to slice
257-
for _, toolInfo := range toolCallMap {
258-
metrics.ToolCalls = append(metrics.ToolCalls, *toolInfo)
259-
}
260-
261-
metrics.TokenUsage = maxTokenUsage
262-
metrics.Turns = turns
263-
264-
copilotCodingAgentLog.Printf("Parsed metrics: tokens=%d, cost=$%.4f, turns=%d",
265-
metrics.TokenUsage, metrics.EstimatedCost, metrics.Turns)
266-
267-
return metrics
268-
}
269-
270-
// extractToolName extracts a tool name from a log line
271-
func extractToolName(line string) string {
272-
// Try to extract tool name from various patterns
273-
patterns := []*regexp.Regexp{
274-
regexp.MustCompile(`(?i)tool[:\s]+([a-zA-Z0-9_-]+)`),
275-
regexp.MustCompile(`(?i)calling[:\s]+([a-zA-Z0-9_-]+)`),
276-
regexp.MustCompile(`(?i)executing[:\s]+([a-zA-Z0-9_-]+)`),
277-
regexp.MustCompile(`(?i)using[:\s]+tool[:\s]+([a-zA-Z0-9_-]+)`),
278-
}
279-
280-
for _, pattern := range patterns {
281-
if matches := pattern.FindStringSubmatch(line); len(matches) > 1 {
282-
return strings.TrimSpace(matches[1])
283-
}
284-
}
285-
286-
return ""
287-
}

pkg/cli/copilot_agent_logs.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package cli
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/github/gh-aw/pkg/workflow"
8+
)
9+
10+
// ParseCopilotCodingAgentLogMetrics extracts metrics from GitHub Copilot coding agent logs
11+
// This is different from Copilot CLI logs and requires specialized parsing
12+
func ParseCopilotCodingAgentLogMetrics(logContent string, verbose bool) workflow.LogMetrics {
13+
copilotCodingAgentLog.Printf("Parsing GitHub Copilot coding agent log metrics: %d bytes", len(logContent))
14+
15+
var metrics workflow.LogMetrics
16+
var maxTokenUsage int
17+
18+
lines := strings.Split(logContent, "\n")
19+
toolCallMap := make(map[string]*workflow.ToolCallInfo)
20+
var currentSequence []string
21+
turns := 0
22+
23+
// GitHub Copilot coding agent log patterns
24+
// These patterns are designed to match the specific log format of the agent
25+
turnPattern := regexp.MustCompile(`(?i)task.*iteration|agent.*turn|step.*\d+`)
26+
toolCallPattern := regexp.MustCompile(`(?i)tool.*call|executing.*tool|calling.*(\w+)`)
27+
28+
for _, line := range lines {
29+
// Skip empty lines
30+
if strings.TrimSpace(line) == "" {
31+
continue
32+
}
33+
34+
// Count turns based on agent iteration patterns
35+
if turnPattern.MatchString(line) {
36+
turns++
37+
// Start of a new turn, save previous sequence if any
38+
if len(currentSequence) > 0 {
39+
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
40+
currentSequence = []string{}
41+
}
42+
}
43+
44+
// Extract tool calls from agent logs
45+
if matches := toolCallPattern.FindStringSubmatch(line); len(matches) > 1 {
46+
toolName := extractToolName(line)
47+
if toolName != "" {
48+
// Track tool call
49+
if _, exists := toolCallMap[toolName]; !exists {
50+
toolCallMap[toolName] = &workflow.ToolCallInfo{
51+
Name: toolName,
52+
CallCount: 0,
53+
}
54+
}
55+
toolCallMap[toolName].CallCount++
56+
57+
// Add to current sequence
58+
currentSequence = append(currentSequence, toolName)
59+
60+
if verbose {
61+
copilotCodingAgentLog.Printf("Found tool call: %s", toolName)
62+
}
63+
}
64+
}
65+
66+
// Try to extract token usage from JSON format if available
67+
jsonMetrics := workflow.ExtractJSONMetrics(line, verbose)
68+
if jsonMetrics.TokenUsage > 0 || jsonMetrics.EstimatedCost > 0 {
69+
if jsonMetrics.TokenUsage > maxTokenUsage {
70+
maxTokenUsage = jsonMetrics.TokenUsage
71+
}
72+
if jsonMetrics.EstimatedCost > 0 {
73+
metrics.EstimatedCost += jsonMetrics.EstimatedCost
74+
}
75+
}
76+
}
77+
78+
// Add final sequence if any
79+
if len(currentSequence) > 0 {
80+
metrics.ToolSequences = append(metrics.ToolSequences, currentSequence)
81+
}
82+
83+
// Convert tool call map to slice
84+
for _, toolInfo := range toolCallMap {
85+
metrics.ToolCalls = append(metrics.ToolCalls, *toolInfo)
86+
}
87+
88+
metrics.TokenUsage = maxTokenUsage
89+
metrics.Turns = turns
90+
91+
copilotCodingAgentLog.Printf("Parsed metrics: tokens=%d, cost=$%.4f, turns=%d",
92+
metrics.TokenUsage, metrics.EstimatedCost, metrics.Turns)
93+
94+
return metrics
95+
}
96+
97+
// extractToolName extracts a tool name from a log line
98+
func extractToolName(line string) string {
99+
// Try to extract tool name from various patterns
100+
patterns := []*regexp.Regexp{
101+
regexp.MustCompile(`(?i)tool[:\s]+([a-zA-Z0-9_-]+)`),
102+
regexp.MustCompile(`(?i)calling[:\s]+([a-zA-Z0-9_-]+)`),
103+
regexp.MustCompile(`(?i)executing[:\s]+([a-zA-Z0-9_-]+)`),
104+
regexp.MustCompile(`(?i)using[:\s]+tool[:\s]+([a-zA-Z0-9_-]+)`),
105+
}
106+
107+
for _, pattern := range patterns {
108+
if matches := pattern.FindStringSubmatch(line); len(matches) > 1 {
109+
return strings.TrimSpace(matches[1])
110+
}
111+
}
112+
113+
return ""
114+
}

pkg/cli/update_extension_check.go

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"os"
66
"strings"
77

8-
"github.com/cli/go-gh/v2/pkg/api"
98
"github.com/github/gh-aw/pkg/console"
109
"github.com/github/gh-aw/pkg/logger"
1110
"github.com/github/gh-aw/pkg/workflow"
@@ -35,7 +34,7 @@ func ensureLatestExtensionVersion(verbose bool) error {
3534
}
3635

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

8483
return nil
8584
}
86-
87-
// getLatestReleaseVersion queries GitHub API for the latest release version of gh-aw
88-
func getLatestReleaseVersion(verbose bool) (string, error) {
89-
updateExtensionCheckLog.Print("Querying GitHub API for latest release...")
90-
91-
// Create GitHub REST client using go-gh
92-
client, err := api.NewRESTClient(api.ClientOptions{})
93-
if err != nil {
94-
return "", fmt.Errorf("failed to create GitHub client: %w", err)
95-
}
96-
97-
// Query the latest release
98-
var release Release
99-
err = client.Get("repos/github/gh-aw/releases/latest", &release)
100-
if err != nil {
101-
return "", fmt.Errorf("failed to query latest release: %w", err)
102-
}
103-
104-
updateExtensionCheckLog.Printf("Latest release: %s", release.TagName)
105-
return release.TagName, nil
106-
}

pkg/workflow/mcp_config_builtin.go

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -288,88 +288,3 @@ func renderAgenticWorkflowsMCPConfigWithOptions(yaml *strings.Builder, isLast bo
288288
yaml.WriteString(" },\n")
289289
}
290290
}
291-
292-
// renderSafeOutputsMCPConfigTOML generates the Safe Outputs MCP server configuration in TOML format for Codex
293-
// Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized.
294-
// Uses MCP Gateway spec format: container, entrypoint, entrypointArgs, and mounts fields.
295-
func renderSafeOutputsMCPConfigTOML(yaml *strings.Builder) {
296-
yaml.WriteString(" \n")
297-
yaml.WriteString(" [mcp_servers." + constants.SafeOutputsMCPServerID.String() + "]\n")
298-
yaml.WriteString(" type = \"http\"\n")
299-
yaml.WriteString(" url = \"http://host.docker.internal:$GH_AW_SAFE_OUTPUTS_PORT\"\n")
300-
yaml.WriteString(" \n")
301-
yaml.WriteString(" [mcp_servers." + constants.SafeOutputsMCPServerID.String() + ".headers]\n")
302-
yaml.WriteString(" Authorization = \"$GH_AW_SAFE_OUTPUTS_API_KEY\"\n")
303-
}
304-
305-
// renderAgenticWorkflowsMCPConfigTOML generates the Agentic Workflows MCP server configuration in TOML format for Codex
306-
// Per MCP Gateway Specification v1.0.0 section 3.2.1, stdio-based MCP servers MUST be containerized.
307-
// Uses MCP Gateway spec format: container, entrypoint, entrypointArgs, and mounts fields.
308-
func renderAgenticWorkflowsMCPConfigTOML(yaml *strings.Builder, actionMode ActionMode) {
309-
yaml.WriteString(" \n")
310-
yaml.WriteString(" [mcp_servers." + constants.AgenticWorkflowsMCPServerID.String() + "]\n")
311-
312-
containerImage := constants.DefaultAlpineImage
313-
var entrypoint string
314-
var entrypointArgs []string
315-
var mounts []string
316-
317-
if actionMode.IsDev() {
318-
// Dev mode: Use locally built Docker image which includes gh-aw binary and gh CLI
319-
// The Dockerfile sets ENTRYPOINT ["gh-aw"] and CMD ["mcp-server"]
320-
// So we don't need to specify entrypoint or entrypointArgs
321-
containerImage = constants.DevModeGhAwImage
322-
entrypoint = "" // Use container's default ENTRYPOINT
323-
entrypointArgs = nil // Use container's default CMD
324-
// Only mount workspace and temp directory - binary and gh CLI are in the image
325-
mounts = []string{constants.DefaultWorkspaceMount, constants.DefaultTmpGhAwMount}
326-
} else {
327-
// Release mode: Use minimal Alpine image with mounted binaries
328-
// Pass --validate-actor flag to enable role-based access control
329-
entrypoint = "/opt/gh-aw/gh-aw"
330-
entrypointArgs = []string{"mcp-server", "--validate-actor"}
331-
// Mount gh-aw binary, gh CLI binary, workspace, and temp directory
332-
mounts = []string{constants.DefaultGhAwMount, constants.DefaultGhBinaryMount, constants.DefaultWorkspaceMount, constants.DefaultTmpGhAwMount}
333-
}
334-
335-
yaml.WriteString(" container = \"" + containerImage + "\"\n")
336-
337-
// Only write entrypoint if it's specified (release mode)
338-
// In dev mode, use the container's default ENTRYPOINT
339-
if entrypoint != "" {
340-
yaml.WriteString(" entrypoint = \"" + entrypoint + "\"\n")
341-
}
342-
343-
// Only write entrypointArgs if specified (release mode)
344-
// In dev mode, use the container's default CMD
345-
if entrypointArgs != nil {
346-
yaml.WriteString(" entrypointArgs = [")
347-
for i, arg := range entrypointArgs {
348-
if i > 0 {
349-
yaml.WriteString(", ")
350-
}
351-
yaml.WriteString("\"" + arg + "\"")
352-
}
353-
yaml.WriteString("]\n")
354-
}
355-
356-
// Write mounts
357-
yaml.WriteString(" mounts = [")
358-
for i, mount := range mounts {
359-
if i > 0 {
360-
yaml.WriteString(", ")
361-
}
362-
yaml.WriteString("\"" + mount + "\"")
363-
}
364-
yaml.WriteString("]\n")
365-
366-
// Add Docker runtime args:
367-
// - --network host: Enables network access for GitHub API calls (gh CLI needs api.github.com)
368-
// - -w: Sets working directory to workspace for .github/workflows folder resolution
369-
// Security: Use GITHUB_WORKSPACE environment variable instead of template expansion to prevent template injection
370-
yaml.WriteString(" args = [\"--network\", \"host\", \"-w\", \"${GITHUB_WORKSPACE}\"]\n")
371-
372-
// Use env_vars array to reference environment variables instead of embedding secrets
373-
// Include GITHUB_ACTOR for role-based access control and GITHUB_REPOSITORY for repository context
374-
yaml.WriteString(" env_vars = [\"DEBUG\", \"GITHUB_TOKEN\", \"GITHUB_ACTOR\", \"GITHUB_REPOSITORY\"]\n")
375-
}

0 commit comments

Comments
 (0)