agentleFS
jmrplensjmrplens/gitlab-mcp-server/.github/skills/modularize-go-package/SKILL.md

modularize-go-package

Skill47 starsChanged 6 months agoMITView on GitHub

Modularize a monolithic Go package into domain-specific sub-packages. Extracts shared utilities, moves domain files, updates imports, renames types, creates registration functions, and validates compilation+tests at every step. Designed for large-scale refactoring of 50-100+ file packages.

The file

Show its rules and text

We split this skill into the rules it gives an agent

24 rules. About 0 are common, 9 are distinctive, and 15 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 15

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Before invoking this skill, ensure:

    line 18
  2. Very specific:

    All tests pass: go test ./internal/...

    line 21
  3. Very specific:

    All code compiles: go build ./...

    line 22
  4. Distinctive:

    Before starting migration, run this discovery sequence:

    line 51
  5. Very specific:

    Check client-go types first: Run go doc gitlab.com/gitlab-org/api/client-go/v3.{Type} to understand the canonical struct fields and API …

    line 63
  6. Very specific:

    Create ${utilPackage}/ and extract shared code in dependency order:

    line 72
  7. Very specific:

    Create new file in ${utilPackage}/

    line 84
  8. Very specific:

    Keep the old file in ${sourcePackage} temporarily as a forwarding stub:

    line 90
  9. Very specific:

    Update imports to use ${utilPackage} instead of direct references

    line 117
  10. Very specific:

    Do not create package-local RegisterTools or package-level RegisterMeta functions for ordinary GitLab API domains.

    line 153
  11. Distinctive:

    Update type references to match renamed types

    line 159
  12. Very specific:

    Import test helpers from internal/testutil (testutil.NewTestClient, testutil.RespondJSON).

    line 160
  13. Distinctive:

    Update handler function references

    line 161
  14. Very specific:

    Add the domain's ActionSpecs(client) builder to the audited catalog aggregation/generation path.

    line 165
  15. Very specific:

    Delete the original files from ${sourcePackage}/:

    line 169
  16. Distinctive:

    After ALL domains are migrated:

    line 185
  17. Very specific:

    Remove forwarding stubs from ${sourcePackage}/

    line 187
  18. Very specific:

    Remove old utility files (they now live in ${utilPackage}/)

    line 188
  19. Distinctive:

    After completing all migrations:

    line 209
  20. Very specific:

    When modularizing internal/tools/, use this mapping to understand which files belong together and why.

    line 277
  21. Distinctive:

    After migration, each sub-package will import:

    line 314
  22. Distinctive:

    Before migrating each domain:

    line 339
  23. Very specific:

    Inspect client-go types: Run go doc gitlab.com/gitlab-org/api/client-go/v3.{Type} for the domain's key types (e.g., gl.Environment, …

    line 341
  24. Distinctive:

    Never skip a domain just because it lacks documentation.

    line 347

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Modularize Go Package

Primary Directive

Transform a monolithic Go package into a modular structure with domain-specific sub-packages and a shared utilities package. Every step must maintain compilation and test integrity.

Execution Model

This skill operates in atomic migration batches. Each batch moves one domain to its own sub-package. Between batches, the project must compile and all tests must pass. Never move to the next batch until the current one is verified.

Prerequisites

Before invoking this skill, ensure:

  1. Clean git working directory (git status shows no uncommitted changes)
  2. All tests pass: go test ./internal/... -count=1
  3. All code compiles: go build ./...
  4. You have identified the source package and its domain files

Input Parameters

  • ${sourcePackage} — The monolithic package to modularize (e.g., internal/tools)
  • ${utilPackage} — Name for the shared utilities package (e.g., internal/toolutil)
  • ${domains} — Comma-separated list of domains to extract (e.g., branches,commits,issues)

Process

Step 1: Inventory Analysis

Scan the source package and classify every file:

CategoryFilesAction
Shared utilitieserrors.go, pagination.go, logging.go, markdown.go, text.go, meta_tool.go, string_or_int.go, file_utils.go, time_helpers.goExtract to ${utilPackage}
Shared constantsAnnotation variables, format constantsExtract to ${utilPackage}
Domain handlersbranches.go, commits.go, etc.Move to ${sourcePackage}/{domain}/
Domain testsbranches_test.go, commits_test.go, etc.Move with their domain
Test helpershelpers_test.goExtract to internal/testutil (exported NewTestClient, RespondJSON, RespondJSONWithPagination)
Catalog wiringaction_specs.go, catalog aggregationKeep runtime surfaces catalog-backed; do not add package-level meta registration as the final path
Package doc(doc comment in any file)Create ${sourcePackage}/doc.go

CRITICAL: Dynamic Discovery

The client-go API library (gitlab.com/gitlab-org/api/client-go/v3) is the source of truth for domain organization, structures, and field definitions. Do NOT rely only on the tables in this skill or on user-facing tool documentation.

Before starting migration, run this discovery sequence:

# 1. Discover ALL client-go services (defines the universe of possible domains)
go doc gitlab.com/gitlab-org/api/client-go/v3.Client | rg '\s+\w+\s+\*\w+Service'

# 2. List all non-test handler files in the source package (what we actually implement)
rg --files "${sourcePackage}" -g '*.go' -g '!*_test.go' | rg -v '/(errors|pagination|logging|markdown|text|metatool|string_or_int|fileutils|time_helpers|register|helpers)\.go$'

Compare the result against the domain mapping table in this skill. For any file NOT in the table:

  1. Check client-go types first: Run go doc gitlab.com/gitlab-org/api/client-go/v3.{Type} to understand the canonical struct fields and API contracts for that domain
  2. Check client.GL().{Service}.* calls in the source file → determines the sub-package name
  3. Check action_specs.go and catalog aggregation → determines canonical runtime surface status
  4. Check the generated tool reference page of the domain's catalog group (site/src/content/docs/reference/tools/<group>.mdx, one page per group, written by make gen-tool-reference; <group> is the group's tool name without gitlab_ and with hyphens, so gitlab_merge_request is merge-request.mdx) → supplementary user-facing context. Never edit it by hand: if the migration changes what the catalog serves (a tool name, an action ID, a description, a tier or a schema), regenerate it

The sub-package name must align with the client-go service name, not with our file naming.

Step 2: Create Shared Utilities Package

Create ${utilPackage}/ and extract shared code in dependency order:

Order (no circular deps):
1. Pure types with zero internal deps (StringOrInt, time helpers)
2. Types depending only on external libs (PaginationInput, ToolError)
3. Functions depending on types above (wrapErr, paginationFromResponse)
4. Complex utilities (markdown formatters, metatool dispatcher)

For each extracted file:

  1. Create new file in ${utilPackage}/
  2. Change package tools → package toolutil
  3. Export all symbols that are used by domain handlers:
    • Functions: wrapErr → WrapErr
    • Variables: readAnnotations → ReadAnnotations
    • Types remain exported if already exported
  4. Keep the old file in ${sourcePackage} temporarily as a forwarding stub:

    // DEPRECATED: forwarding stub — will be removed when all domains are migrated.
    package tools
    
    import "github.com/jmrplens/gitlab-mcp-server/v3/internal/toolutil"
    
    var wrapErr = toolutil.WrapErr
  5. Verify: go build ./...

Step 3: Migrate Domain (Repeat per Domain)

For each domain in priority order:

3a. Create Sub-Package

mkdir -p ${sourcePackage}/{domain}

3b. Move and Transform Handler File

  1. Copy {domain}.go → ${sourcePackage}/{domain}/{domain}.go
  2. Change package declaration: package tools → package {domain}
  3. Update imports to use ${utilPackage} instead of direct references
  4. Rename types — remove domain prefix (the package name provides context):
    • BranchCreateInput → CreateInput
    • BranchOutput → Output
    • BranchListOutput → ListOutput
  5. Export handler functions — remove domain prefix, capitalize:
    • branchCreate → Create
    • branchList → List
    • branchGet → Get
  6. Replace internal utility calls:
    • wrapErr(...) → toolutil.WrapErr(...)
    • markdownForResult(...) → toolutil.MarkdownForResult(...)
    • logToolCallAll(...) → toolutil.LogToolCallAll(...)
    • readAnnotations → toolutil.ReadAnnotations

3c. Create ActionSpec File

Create ${sourcePackage}/{domain}/action_specs.go:

// Copyright (c) 2026 José M. Requena Plens
// SPDX-License-Identifier: MIT

package {domain}

import (
   gitlabclient "github.com/jmrplens/gitlab-mcp-server/v3/internal/gitlab"
   "github.com/jmrplens/gitlab-mcp-server/v3/internal/toolutil"
)

// ActionSpecs returns canonical specs for {domain} actions.
func ActionSpecs(client *gitlabclient.Client) []toolutil.ActionSpec {
   // ... specs moved from the old registration metadata and wired to handlers
}

Do not create package-local RegisterTools or package-level RegisterMeta functions for ordinary GitLab API domains. The root runtime must use catalog projection from ActionSpecs.

3d. Move and Transform Test File

  1. Copy {domain}_test.go → ${sourcePackage}/{domain}/{domain}_test.go
  2. Change package: package tools → package {domain} (or package {domain}_test for black-box)
  3. Update type references to match renamed types
  4. Import test helpers from internal/testutil (testutil.NewTestClient, testutil.RespondJSON); do not recreate them locally
  5. Update handler function references

3e. Update Catalog Aggregation

Add the domain's ActionSpecs(client) builder to the audited catalog aggregation/generation path. Validate that RegisterAll projects individual tools from the catalog rather than calling domain RegisterTools directly.

3f. Remove Old Files

Delete the original files from ${sourcePackage}/:

  • {domain}.go
  • {domain}_test.go

3g. Verify

go build ./...
golangci-lint run --build-tags e2e ./...
go test ./${sourcePackage}/{domain}/ -count=1 -v
go test ./${sourcePackage}/ -count=1

Step 4: Clean Up Forwarding Stubs

After ALL domains are migrated:

  1. Remove forwarding stubs from ${sourcePackage}/
  2. Remove old utility files (they now live in ${utilPackage}/)
  3. Final verification:

    go build ./...
    golangci-lint run --build-tags e2e ./...
    go test ./internal/... -count=1

Step 5: Update Entry Point

Verify cmd/server/main.go needs no change for the moved domain: it builds the catalog through internal/tools (gitlabtools.RegisterAll(server, client, tier) for the individual surface, BuildActionCatalog plus the dynamiccatalog / dynamic packages for the default surface) and imports only the surface-level packages (internal/tools, internal/tools/actioncatalog, internal/tools/dynamic, internal/tools/dynamiccatalog, internal/tools/toolvisibility) plus internal/tools/health for the server's own health probe; no GitLab API domain package is imported there:

import gitlabtools "github.com/jmrplens/gitlab-mcp-server/v3/internal/tools"

// gitlabtools.RegisterAll(server, client, tier) projects the catalog; a new domain arrives through action_specs.go

Validation Checklist

After completing all migrations:

  • go build ./... — zero errors
  • golangci-lint run --build-tags e2e ./... — zero warnings
  • go test ./internal/... -count=1 — all pass
  • No import cycles: go list ./... or manual review
  • cmd/server/main.go unchanged (it never imports a domain package)
  • Each sub-package has: doc.go, handler file, action_specs.go, markdown formatter, and test file named after the module it tests (make check-test-file-names)
  • ${utilPackage} has no imports from domain sub-packages
  • Domain sub-packages import each other only for a row the imported package owns, never in a cycle, and mirror a shape they only resemble (ADR-0004, amended POS-004)

Multi-File Domain Handling

A domain that spans several source files can either stay one package with several files or split into one package per sub-domain. This project did both, and the current tree is the reference:

Merge Requests (6 monolith files → 6 sub-packages)

merge_requests.go      → mergerequests/merge_requests.go
mr_notes.go            → mrnotes/
mr_discussions.go      → mrdiscussions/
mr_changes.go          → mrchanges/
mr_approvals.go        → mrapprovals/
mr_draft_notes.go      → mrdraftnotes/

Each sub-package owns its handlers and action_specs.go; buildMergeRequestActionSpecs and buildMRReviewActionSpecs in internal/tools/action_specs.go merge them into the gitlab_merge_request catalog group, so one meta-tool still fronts them all.

Packages (3 files → 1 sub-package)

packages.go            → packages/packages.go
packages_composite.go  → packages/packages_composite.go
packages_stream.go     → packages/packages_stream.go

Error Recovery

Compilation Error After Move

# Most common: unexported symbol
./internal/tools/branches/branches.go:15: undefined: wrapErr
→ Fix: Change to toolutil.WrapErr

# Missing import
./internal/tools/branches/branches.go:3: imported and not used
→ Fix: Remove unused import, add missing one

# Circular import
package gitlab.example.com/.../tools imports gitlab.example.com/.../tools/branches imports gitlab.example.com/.../tools
→ Fix: Extract shared code to toolutil, break the cycle

Test Failure After Move

# Test helper not found
./internal/tools/branches/branches_test.go:10: undefined: newTestClient
→ Fix: Import internal/testutil and call testutil.NewTestClient

# Type mismatch
cannot use BranchOutput as tools.BranchOutput
→ Fix: Update test to use new type name (Output instead of BranchOutput)

GitLab API Domain Reference

When modularizing internal/tools/, use this mapping to understand which files belong together and why. Each sub-package should correspond to a coherent GitLab API domain.

Service-to-SubPackage Mapping

The project uses gitlab.com/gitlab-org/api/client-go/v3 at the version go.mod pins (v3.14.0 when this was written). Each client.GL().{Service} call tells you which API domain a handler belongs to. The table records the original monolith-to-sub-package mapping; the migration is complete and internal/tools/ now holds the root package and 180 sub-packages, so treat it as the pattern, not the inventory:

Sub-Packageclient-go Services UsedSource Files
branches/Branches, ProtectedBranchesbranches.go
tags/Tagstags.go
commits/Commitscommits.go
files/RepositoryFilesfiles.go
repository/Repositoriesrepository.go
projects/Projectsrepositories.go (misnamed — rename during move)
mergerequests/ (+ mrnotes/, mrdiscussions/, mrchanges/, mrapprovals/, mrdraftnotes/)MergeRequests, MergeRequestApprovals, Notes, Discussions, DraftNotesmerge_requests.go, mr_notes.go, mr_discussions.go, mr_changes.go, mr_approvals.go, mr_draft_notes.go
issues/Issues, Notesissues.go, issue_notes.go
labels/Labelslabels.go
milestones/Milestonesmilestones.go
members/ProjectMembersmembers.go
groups/Groupsgroups.go
pipelines/Pipelinespipelines.go
jobs/Jobsjobs.go
releases/Releases, ReleaseLinksreleases.go, release_links.go
search/Searchsearch.go
users/Usersusers.go
packages/Packages, GenericPackagespackages.go, packages_composite.go, packages_stream.go
uploads/ProjectMarkdownUploadsuploads.go
wikis/Wikiswikis.go
todos/Todostodos.go
health/Versionhealth.go
environments/Environmentsenvironments.go
elicitationtools/(MCP-only, no GitLab API)elicitation_tools.go

⚠️ This table may be incomplete. Always scan the source package for files not listed here before starting a migration session. Any unlisted handler file is a new domain to add to the plan.

client-go Import Patterns

After migration, each sub-package will import:

import (
    gl "gitlab.com/gitlab-org/api/client-go/v3"
   gitlabclient "github.com/jmrplens/gitlab-mcp-server/v3/internal/gitlab"
   "github.com/jmrplens/gitlab-mcp-server/v3/internal/toolutil"
)

Preserve these client-go calling patterns exactly:

  • CRUD: result, resp, err := client.GL().{Service}.{Method}(args..., gl.WithContext(ctx))
  • Delete: _, err := client.GL().{Service}.Delete{Resource}(id, gl.WithContext(ctx))
  • Low-level HTTP (packages only): client.GL().NewRequest(...) + client.GL().Do(...)
  • Option structs: &gl.List{Resource}Options{...} — these never change, they come from client-go

Naming Fix During Migration

The file repositories.go contains Projects CRUD operations (uses client.GL().Projects.*), NOT repository operations. When moving to the projects/ sub-package, rename it to projects.go. The actual repository operations (tree, compare) are in repository.go and use client.GL().Repositories.*.

Reference Documentation

The client-go API library is the source of truth for domain structure and field definitions. Our source code implements a subset of it. The site's per-domain tool reference (site/src/content/docs/reference/tools/, generated from the catalog) is supplementary user-facing documentation, not the canonical field map.

Before migrating each domain:

  1. Inspect client-go types: Run go doc gitlab.com/gitlab-org/api/client-go/v3.{Type} for the domain's key types (e.g., gl.Environment, gl.CreateEnvironmentOptions). This defines the canonical fields, types, and API contract.
  2. Read the source file(s) in internal/tools/{domain}.go — shows our implementation: which client-go fields we expose, our Input/Output structs, and client.GL().{Service} calls.
  3. Check action_specs.go and catalog aggregation for runtime exposure. Files absent from the catalog are in-progress — still migrate them, but note the gap.
  4. Read the generated tool reference page of the domain's catalog group (site/src/content/docs/reference/tools/<group>.mdx): supplementary user-facing context. A domain not yet in the catalog has no page, and the combination of steps 1+2 provides everything needed.
  5. Discover new domains by scanning *.go files AND running go doc on the client to find services we haven't wrapped yet.

Never skip a domain just because it lacks documentation. The client-go types have all the information needed.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.