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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions