Skip to content

[refactor] Semantic function clustering: Antigravity/Gemini engine clone + duplicate render/parse helpersΒ #46316

Description

@github-actions

πŸ”§ Semantic Function Clustering Analysis

Analysis of repository: github/gh-aw β€” 1,102 non-test Go files across pkg/

Executive Summary

Semantic clustering + Serena/grep-verified body comparison across the codebase found a small set of high-confidence, actionable refactoring opportunities. The dominant finding is a wholesale clone of the Gemini CLI engine into the Antigravity engine (five paired files, ~90–95% identical bodies), which has already begun to drift and now carries latent behavioral inconsistency. Secondary findings are localized near-duplicate render/parse helpers in pkg/cli, pkg/console, and pkg/parser.

Good news worth noting: the parse*Config safe-output family (~40 functions) is already well consolidated behind parseConfigScaffold, and provider-specific ParseLogMetrics correctly delegate to a shared parseStatsJSONLMetrics. This report focuses only on what remains.

Metrics

Metric Value
Non-test Go files analyzed 1,102 (focus: pkg/workflow 453, pkg/cli 357, pkg/parser 52, pkg/console 29)
Verified duplicate clusters 8
Files in the Antigravity/Gemini clone 5 pairs
Scattered-helper patterns 3
Detection method Serena/gopls semantic lookup + grep signature clustering + manual body diff

Priority 1 β€” High Impact: Antigravity / Gemini engine clone

The entire antigravity_* engine is a near-verbatim copy of gemini_*. Each pair differs only in engine name, args, and API-key/base-URL env-var literals. This is the single highest-value refactor.

Verified paired duplicates (click to expand)

1a. GetExecutionSteps (~200 lines, >90% identical)

  • pkg/workflow/antigravity_engine.go:155 β€” func (e *AntigravityEngine) GetExecutionSteps(workflowData *WorkflowData, logFile string) []GitHubActionStep
  • pkg/workflow/gemini_engine.go:157 β€” func (e *GeminiEngine) GetExecutionSteps(...)

Same firewall block, same non-firewall command template, same env-map construction, same filtering tail. The command template is byte-identical apart from the injected command literal.

1b. GetRequiredSecretNames (byte-for-byte apart from one literal) β€” confirmed by direct read

  • pkg/workflow/antigravity_engine.go:50 (secrets := []string{"ANTIGRAVITY_API_KEY"})
  • pkg/workflow/gemini_engine.go:50 (secrets := []string{"GEMINI_API_KEY"})

Both then run the identical collectCommonMCPSecrets(...) append and the same hasGitHubTool -> GITHUB_MCP_SERVER_TOKEN branch.

1c. Settings-step generators (~95% identical)

  • pkg/workflow/antigravity_tools.go:130 β€” generateAntigravitySettingsStep
  • pkg/workflow/gemini_tools.go:125 β€” generateGeminiSettingsStep

Same tools-core plumbing, same JSON config shape, same jq-merge shell script; differ only in .antigravity/.gemini dir and GH_AW_*_BASE_CONFIG env name.

1d. RenderMCPConfig + surrounding engine methods (identical bodies)

  • pkg/workflow/antigravity_mcp.go:13 and pkg/workflow/gemini_mcp.go:13, both delegating to renderDefaultJSONMCPConfig(...). The GetPreBundleSteps, GetDeclaredOutputFiles, GetAgentManifestFiles/PathPrefixes, and GetSecretValidationStep pairs are all clone-per-engine.

1e. ⚠️ Already-diverged clone (latent bug) β€” computeToolsCore

  • pkg/workflow/antigravity_tools.go:43 computeAntigravityToolsCore
  • pkg/workflow/gemini_tools.go:44 computeGeminiToolsCore

Same output contract, but the copies have drifted: Antigravity factored the bash logic into appendBashTools (with "wildcard anywhere wins" semantics) while Gemini still inlines an earlier loop with different discard-on-wildcard behavior. This is a real behavioral inconsistency between two engines that should behave identically.

Recommendation: Introduce a shared embeddable base type (e.g. googleCLIEngine) carrying the common behavior, parameterized by per-engine constants (engine name, primary secret, config dir, base-config env var, CLI args). The two engines become thin wrappers. Unify computeToolsCore on the single appendBashTools helper first β€” that one closes an active divergence, not just duplication.
Estimated effort: 4–6 hours. Benefit: removes ~5 clone pairs and eliminates the tools-core behavioral drift.

Priority 2 β€” Medium Impact: localized duplicate render/parse helpers

pkg/cli findings (click to expand)

2a. Poutine output rendering copied verbatim

  • pkg/cli/poutine.go:275 parseAndDisplayPoutineOutput vs pkg/cli/poutine.go:385 parseAndDisplayPoutineOutputForDirectory

Identical JSON preamble plus a ~60-line per-finding rendering loop (severity defaulting, startLine := max(1, lineNum-2) context window, console.CompilerError construction). Only per-file-filter vs group-by-file differs.
β†’ Extract renderPoutineFindings(output, findings, fileLines) used by both.

2b. Scattered "read file -> context lines -> CompilerError" helper (3+ files)

  • pkg/cli/poutine.go:337 & :480, pkg/cli/runner_guard.go:226, pkg/cli/zizmor.go:259

The read-source -> split-lines -> build context-around-lineNum -> emit console.CompilerError block is duplicated. Additionally the path-traversal guard (filepath.Clean -> Abs -> Rel -> reject ..) is copied nearly verbatim between poutine.go:420 and runner_guard.go:182.
β†’ Extract extractContextLines(fileLines, lineNum) and validateFileWithinRoot(gitRoot, filePath).

2c. Firewall timeline row renderers (~95% identical)

  • pkg/cli/gateway_logs_timeline_render.go:218 renderFirewallNetworkAllowedRow vs :241 renderFirewallNetworkBlockedRow β€” differ only in event-kind constant and initial status value.
    β†’ Unify into renderFirewallNetworkRow(evt, kind, defaultStatus).

2d. TSV renderers share skeleton + identical insights footer

  • pkg/cli/logs_format_tsv.go:31 renderLogsTSV vs :115 renderLogsTSVVerbose β€” same row-normalization loop and identical # insights: observability block; verbose is a column super-set.
    β†’ Drive both from one column-descriptor list; factor the shared footer.

2e. Org report header block repeated across three commands (weakest of the CLI set)

  • deploy_org.go:38 renderOrgDeployReport, update_org.go:152 renderOrgPreviewReport, upgrade_org.go:116 renderOrgUpgradeReport β€” share the applying/dry-run header idiom; per-item bodies legitimately differ.
    β†’ Extract renderOrgReportHeader(...); keep item bodies distinct.
pkg/console & pkg/parser findings (click to expand)

2f. Two byte-size formatters (confirmed both wrap scaleBinaryBytes)

  • pkg/console/progress_shared.go:6 formatBytes vs pkg/console/format.go:21 FormatFileSize β€” differ only cosmetically (space/precision/unit-list).
    β†’ Collapse into one formatter with a small style option.

2g. Two compact-number formatters in one file

  • pkg/console/render.go:674 FormatNumber (k/M/B, "0" for zero) vs :727 FormatTokens (K/M, "-" for zero) β€” same concept, divergent conventions producing inconsistent UI output.
    β†’ Unify onto one parameterized helper (suffix case + zero placeholder + precision).

2h. Near-duplicate cron handlers

  • pkg/parser/schedule_fuzzy_scatter.go: handleDaily(297)/handleDailyWeekdays(287), handleHourly(322)/handleHourlyWeekdays(307) β€” each pair >80% identical, differing only in the cron day field (* * * vs * * 1-5).
    β†’ Parameterize each pair with a dayField string argument.

2i. Redundant shadowing wrapper

  • pkg/parser/schema_suggestions.go:231 FindClosestMatches only adds two log lines around stringutil.FindClosestMatches (same name/signature/doc copied verbatim from pkg/stringutil/fuzzy_match.go:19).
    β†’ Have callers use stringutil.FindClosestMatches directly, or keep only if the logging is required.

Minor twin (Priority 3): pkg/workflow/add_labels.go:18 parseAddLabelsConfig vs remove_labels.go:18 parseRemoveLabelsConfig are identical except the "add-labels"/"remove-labels" literal β€” candidate for a generic parseAllowBlockLabelConfig[T].

Explicitly checked and ruled out

To keep signal high, these were investigated and dropped as not problematic duplication: //go:build wasm variant files (console_wasm.go, github_wasm.go), the console.FormatXMessage family (trivial distinct public APIs), typeutil.ParseIntValue vs ConvertToInt (intentional strict-vs-lenient semantics), download_workflow.go git-archive vs sparse-clone strategies, deps_outdated.go thin wrappers, and agentdrain Save/Load inverse pairs.

Implementation Checklist

  • P1: Unify computeAntigravityToolsCore/computeGeminiToolsCore on appendBashTools (closes active divergence) β€” do this first
  • P1: Introduce shared googleCLIEngine base for the Antigravity/Gemini engine clone (execution steps, secrets, settings, MCP config)
  • P2: Extract renderPoutineFindings, extractContextLines, validateFileWithinRoot in pkg/cli
  • P2: Unify firewall row renderers and TSV renderers
  • P2: Consolidate formatBytes/FormatFileSize and FormatNumber/FormatTokens in pkg/console
  • P2: Parameterize cron weekday handlers; drop parser.FindClosestMatches shadow
  • Update/extend tests for each consolidation; verify no behavior change (except intended computeToolsCore alignment)

Analysis Metadata

  • Total Go files analyzed: 1,102 (non-test, pkg/)
  • Verified duplicate clusters: 8
  • Scattered-helper patterns: 3
  • Detection method: Serena/gopls semantic analysis + naming-pattern clustering + manual body diff
  • Analysis date: 2026-07-18

Generated by πŸ”§ Semantic Function Refactoring Β· 457.9 AIC Β· βŒ– 16.5 AIC Β· ⊞ 9.4K Β· β—·

  • expires on Jul 19, 2026, 4:13 PM UTC-08:00

Metadata

Metadata

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions