Skip to content

Commit b00407b

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

9 files changed

Lines changed: 273 additions & 334 deletions

‎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
varcopilotCodingAgentLog=logger.New("cli:copilot_agent")
@@ -179,109 +178,3 @@ func readLogHeader(path string, maxBytes int) (string, error) {
179178

180179
returnstring(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-
funcParseCopilotCodingAgentLogMetrics(logContentstring, verbosebool) workflow.LogMetrics {
186-
copilotCodingAgentLog.Printf("Parsing GitHub Copilot coding agent log metrics: %d bytes", len(logContent))
187-
188-
varmetrics workflow.LogMetrics
189-
varmaxTokenUsageint
190-
191-
lines:=strings.Split(logContent, "\n")
192-
toolCallMap:=make(map[string]*workflow.ToolCallInfo)
193-
varcurrentSequence []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:=rangelines {
202-
// Skip empty lines
203-
ifstrings.TrimSpace(line) =="" {
204-
continue
205-
}
206-
207-
// Count turns based on agent iteration patterns
208-
ifturnPattern.MatchString(line) {
209-
turns++
210-
// Start of a new turn, save previous sequence if any
211-
iflen(currentSequence) >0 {
212-
metrics.ToolSequences=append(metrics.ToolSequences, currentSequence)
213-
currentSequence= []string{}
214-
}
215-
}
216-
217-
// Extract tool calls from agent logs
218-
ifmatches:=toolCallPattern.FindStringSubmatch(line); len(matches) >1 {
219-
toolName:=extractToolName(line)
220-
iftoolName!="" {
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-
ifverbose {
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-
ifjsonMetrics.TokenUsage>0||jsonMetrics.EstimatedCost>0 {
242-
ifjsonMetrics.TokenUsage>maxTokenUsage {
243-
maxTokenUsage=jsonMetrics.TokenUsage
244-
}
245-
ifjsonMetrics.EstimatedCost>0 {
246-
metrics.EstimatedCost+=jsonMetrics.EstimatedCost
247-
}
248-
}
249-
}
250-
251-
// Add final sequence if any
252-
iflen(currentSequence) >0 {
253-
metrics.ToolSequences=append(metrics.ToolSequences, currentSequence)
254-
}
255-
256-
// Convert tool call map to slice
257-
for_, toolInfo:=rangetoolCallMap {
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-
returnmetrics
268-
}
269-
270-
// extractToolName extracts a tool name from a log line
271-
funcextractToolName(linestring) 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:=rangepatterns {
281-
ifmatches:=pattern.FindStringSubmatch(line); len(matches) >1 {
282-
returnstrings.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+
funcParseCopilotCodingAgentLogMetrics(logContentstring, verbosebool) workflow.LogMetrics {
13+
copilotCodingAgentLog.Printf("Parsing GitHub Copilot coding agent log metrics: %d bytes", len(logContent))
14+
15+
varmetrics workflow.LogMetrics
16+
varmaxTokenUsageint
17+
18+
lines:=strings.Split(logContent, "\n")
19+
toolCallMap:=make(map[string]*workflow.ToolCallInfo)
20+
varcurrentSequence []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:=rangelines {
29+
// Skip empty lines
30+
ifstrings.TrimSpace(line) =="" {
31+
continue
32+
}
33+
34+
// Count turns based on agent iteration patterns
35+
ifturnPattern.MatchString(line) {
36+
turns++
37+
// Start of a new turn, save previous sequence if any
38+
iflen(currentSequence) >0 {
39+
metrics.ToolSequences=append(metrics.ToolSequences, currentSequence)
40+
currentSequence= []string{}
41+
}
42+
}
43+
44+
// Extract tool calls from agent logs
45+
ifmatches:=toolCallPattern.FindStringSubmatch(line); len(matches) >1 {
46+
toolName:=extractToolName(line)
47+
iftoolName!="" {
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+
ifverbose {
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+
ifjsonMetrics.TokenUsage>0||jsonMetrics.EstimatedCost>0 {
69+
ifjsonMetrics.TokenUsage>maxTokenUsage {
70+
maxTokenUsage=jsonMetrics.TokenUsage
71+
}
72+
ifjsonMetrics.EstimatedCost>0 {
73+
metrics.EstimatedCost+=jsonMetrics.EstimatedCost
74+
}
75+
}
76+
}
77+
78+
// Add final sequence if any
79+
iflen(currentSequence) >0 {
80+
metrics.ToolSequences=append(metrics.ToolSequences, currentSequence)
81+
}
82+
83+
// Convert tool call map to slice
84+
for_, toolInfo:=rangetoolCallMap {
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+
returnmetrics
95+
}
96+
97+
// extractToolName extracts a tool name from a log line
98+
funcextractToolName(linestring) 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:=rangepatterns {
108+
ifmatches:=pattern.FindStringSubmatch(line); len(matches) >1 {
109+
returnstrings.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
iferr!=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
returnnil
8584
}
86-
87-
// getLatestReleaseVersion queries GitHub API for the latest release version of gh-aw
88-
funcgetLatestReleaseVersion(verbosebool) (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-
iferr!=nil {
94-
return"", fmt.Errorf("failed to create GitHub client: %w", err)
95-
}
96-
97-
// Query the latest release
98-
varreleaseRelease
99-
err=client.Get("repos/github/gh-aw/releases/latest", &release)
100-
iferr!=nil {
101-
return"", fmt.Errorf("failed to query latest release: %w", err)
102-
}
103-
104-
updateExtensionCheckLog.Printf("Latest release: %s", release.TagName)
105-
returnrelease.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-
funcrenderSafeOutputsMCPConfigTOML(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-
funcrenderAgenticWorkflowsMCPConfigTOML(yaml*strings.Builder, actionModeActionMode) {
309-
yaml.WriteString(" \n")
310-
yaml.WriteString(" [mcp_servers."+constants.AgenticWorkflowsMCPServerID.String() +"]\n")
311-
312-
containerImage:=constants.DefaultAlpineImage
313-
varentrypointstring
314-
varentrypointArgs []string
315-
varmounts []string
316-
317-
ifactionMode.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-
ifentrypoint!="" {
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-
ifentrypointArgs!=nil {
346-
yaml.WriteString(" entrypointArgs = [")
347-
fori, arg:=rangeentrypointArgs {
348-
ifi>0 {
349-
yaml.WriteString(", ")
350-
}
351-
yaml.WriteString("\""+arg+"\"")
352-
}
353-
yaml.WriteString("]\n")
354-
}
355-
356-
// Write mounts
357-
yaml.WriteString(" mounts = [")
358-
fori, mount:=rangemounts {
359-
ifi>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)