Source profileQuality 91/100

speakeasy-api/gram/.agents/skills/glint/SKILL.md

glint

Conventions for authoring or editing analyzers in the `glint/` Go static-analysis package — Gram's custom golangci-lint plugin built on `go/analysis`. Activate this skill whenever the task involves adding, modifying, or testing a `glint` analyzer (new rule key, new diagnostic, settings struct, fixture under `glint/testdata/`, wiring in `BuildAnalyzers`), even if the user does not say "glint" explicitly — phrases like "add a lint rule", "write a custom analyzer", "go/analysis", or "enforce X via

Source repository stars
266
Declared platforms
0
Static risk flags
2
Last source update
2026-08-25
Source checked
2026-08-25

Decision brief

What it does: where it fits

When adding a new analyzer:

Best for

    Not for

    • Tasks that require unconfirmed production actions or broad system permissions.
    • Environments where the pinned source and install steps cannot be inspected.

    Compatibility matrix

    Platform support, with evidence labels

    PlatformStatusEvidenceWhat to check
    CodexNot declaredNo explicit evidencePortability before use
    Claude CodeNot declaredNo explicit evidencePortability before use
    CursorNot declaredNo explicit evidencePortability before use
    Gemini CLINot declaredNo explicit evidencePortability before use
    Open the compatibility checker

    Installation

    Inspect first. Install second.

    The source command is displayed only when detected. A safe inspection prompt is always available so your agent can explain every action before execution.

    Source-detected install commandSource
    npx skills add https://github.com/speakeasy-api/gram --skill ".agents/skills/glint"
    Safe inspection promptEditorial

    Inspect the Agent Skill "glint" from https://github.com/speakeasy-api/gram/blob/8af0601cf530721aaa686c2718d76e7990226d33/.agents/skills/glint/SKILL.md at commit 8af0601cf530721aaa686c2718d76e7990226d33. List every install step, command, network request, credential, file read/write, external action, and rollback step. Explain whether it fits my task. Do not install or execute anything until I approve.

    Workflow

    What the source asks the agent to do

    1. 01

      Quick start

      When adding a new analyzer:

      Pick a kebab-case rule key and a matching snakecase.go file name in glint/. See Naming conventions.Define Settings with Disabled bool. Add nothing else unless there's a concrete user-facing reason. See Settings and ignore mechanisms.Implement detection in newAnalyzer(rule Settings) analysis.Analyzer. Prefer type-based checks over AST shape, and AST shape over string matching. See Detection methodology.
    2. 02

      Detection methodology

      Strongly prefer type-based detection over AST-shape matching, and AST-shape matching over source-string matching. Type-based checks are robust against renames, aliases, vendored packages, and import-path quirks; AST shape is robust against most refactors but blind to type identi…

      The callback returns nothing, so there is no return false to prune a subtree the way ast.Inspect allows. If a rule genuinely depends on pruning descent (matching a node and deliberately skipping its children, for exampl…Preorder walks every file in the package with no file boundary, so any per-file scoping has to happen per node instead of around a pass.Files loop. When a rule applies only to certain files, filter inside the callback v…Pulling the inspector from pass.ResultOf only works if the analyzer declared inspect.Analyzer in its Requires; otherwise the result is nil and the type assertion panics. This bites helpers shared across analyzers, since…
    3. 03

      Prefer types via pass.TypesInfo

      glint/norepofieldsinservice.go walks struct fields, resolves each field's type via pass.TypesInfo.TypeOf, then narrows through types.Pointer → types.Named to identify a sqlc-generated repo handle:

      glint/norepofieldsinservice.go walks struct fields, resolves each field's type via pass.TypesInfo.TypeOf, then narrows through types.Pointer → types.Named to identify a sqlc-generated repo handle:Reach for type assertions on types.Type (types.Named, types.Pointer, types.Slice, types.Map, types.Interface); use pass.TypesInfo.Uses[ident] / pass.TypesInfo.Defs[ident] to resolve identifiers to their declared types.O…
    4. 04

      AST shape is acceptable when types are not load-bearing

      When the rule is fundamentally about the shape of code rather than its types, AST matching is fine. glint/noanonymousdefer.go walks ast.DeferStmt and asserts the call target is ast.FuncLit:

      When the rule is fundamentally about the shape of code rather than its types, AST matching is fine. glint/noanonymousdefer.go walks ast.DeferStmt and asserts the call target is ast.FuncLit:There's no types.Type that captures "anonymous deferred function" — the property only exists at the AST level — so AST matching is the right tool.
    5. 05

      Traversal: depend on inspect.Analyzer for deep walks

      When a rule needs to find nodes anywhere in the tree (calls, defers, type specs nested in functions, etc.), declare a dependency on the shared inspector rather than hand-rolling for , file := range pass.Files { ast.Inspect(file, ...) }. inspect.Analyzer parses each file once and…

      The callback returns nothing, so there is no return false to prune a subtree the way ast.Inspect allows. If a rule genuinely depends on pruning descent (matching a node and deliberately skipping its children, for exampl…Preorder walks every file in the package with no file boundary, so any per-file scoping has to happen per node instead of around a pass.Files loop. When a rule applies only to certain files, filter inside the callback v…Pulling the inspector from pass.ResultOf only works if the analyzer declared inspect.Analyzer in its Requires; otherwise the result is nil and the type assertion panics. This bites helpers shared across analyzers, since…

    Permission review

    Static risk signals and limitations

    Reads files

    low · line 73

    The documentation asks the agent to read local files, directories, or repositories.

    When a rule needs to find nodes anywhere in the tree (calls, defers, type specs nested in functions, etc.), declare a dependency on the shared inspector rather than hand-rolling `for _, file := range pass.Files { ast.Inspect(file, ...) }`.

    Writes files

    medium · line 177

    The documentation asks the agent to create, modify, or delete local files.

    pass.ReportRangef(field, "field %q in %s has type %s which is sqlc-generated; services should use *pgxpool.Pool and create repo instances in methods",

    Writes files

    medium · line 247

    The documentation asks the agent to create, modify, or delete local files.

    `Remove(fset, file, path) (analysis.TextEdit, bool)` — emits an edit that deletes the import line for `path`.

    Evidence record

    Why each signal appears

    EvidenceSourceComputedTestedEditorial
    SignalValueEvidence typeMeaning
    Quality score91/100ComputedDocumentation, specificity, maintenance, and trust rules
    Repository stars266SourceRepository attention, not individual Skill quality
    Compatibility0 platformsSourceDeclared in the catalog source record
    Usage guideautomated source guideEditorialGenerated or reviewed according to the visible evidence level

    Pinned source

    Provenance and original SKILL.md

    Repository
    speakeasy-api/gram
    Skill path
    .agents/skills/glint/SKILL.md
    Commit
    8af0601cf530721aaa686c2718d76e7990226d33
    License
    AGPL-3.0
    Collected
    2026-08-25
    Default branch
    main
    View the original SKILL.md

    glint is Gram's package of custom go/analysis analyzers, and gcl is the golangci-lint custom-build configuration that loads glint as a plugin. Together they automate enforcement of project coding conventions and bug-prevention rules so the same feedback isn't re-litigated in PR review.

    • Plugin entry point: glint/plugin.go — registers the plugin via register.Plugin("glint", New), defines the settings/ruleSettings structs, and lists every analyzer in BuildAnalyzers.
    • gcl wiring: server/.custom-gcl.yml — declares github.com/speakeasy-api/gram/glint as the imported plugin module for the custom golangci-lint binary.

    The current set of analyzers and their rule keys is the source of truth in BuildAnalyzers. Read that function before adding a new one.

    Quick start

    When adding a new analyzer:

    1. Pick a kebab-case rule key and a matching snake_case.go file name in glint/. See Naming conventions.
    2. Define <rule>Settings with Disabled bool. Add nothing else unless there's a concrete user-facing reason. See Settings and ignore mechanisms.
    3. Implement detection in new<Rule>Analyzer(rule <rule>Settings) *analysis.Analyzer. Prefer type-based checks over AST shape, and AST shape over string matching. See Detection methodology.
    4. Write the diagnostic with imperative tone and the offending identifier inlined via %q. See Diagnostic message style.
    5. Wire the rule into glint/plugin.go: add a ruleSettings field with the kebab-case JSON tag, then an if !p.settings.Rules.<X>.Disabled block in BuildAnalyzers.
    6. Add <rule>_test.go with analysistest.Run(...) and a fixture under glint/testdata/src/<fixture>/. Update disabledAllRulesPlugin() and the count in TestBuildAnalyzersAllEnabled in glint/plugin_test.go. See Testing.

    Detection methodology

    Strongly prefer type-based detection over AST-shape matching, and AST-shape matching over source-string matching. Type-based checks are robust against renames, aliases, vendored packages, and import-path quirks; AST shape is robust against most refactors but blind to type identity; raw string matching is fragile and should only be reached for when the signal genuinely lives in source text rather than the type system.

    Prefer types via pass.TypesInfo

    glint/no_repo_fields_in_service.go walks struct fields, resolves each field's type via pass.TypesInfo.TypeOf, then narrows through *types.Pointer*types.Named to identify a sqlc-generated repo handle:

    fieldType := pass.TypesInfo.TypeOf(field.Type)
    if fieldType == nil {
        continue
    }
    
    ptr, ok := fieldType.(*types.Pointer)
    if !ok {
        continue
    }
    
    named, ok := ptr.Elem().(*types.Named)
    if !ok {
        continue
    }
    
    if !isSqlcGenerated(pass.Fset, named.Obj(), cache) {
        continue
    }
    

    Reach for type assertions on types.Type (*types.Named, *types.Pointer, *types.Slice, *types.Map, *types.Interface); use pass.TypesInfo.Uses[ident] / pass.TypesInfo.Defs[ident] to resolve identifiers to their declared types.Object; inspect method receivers via Recv() when checking method-set rules. The plugin advertises register.LoadModeTypesInfo from GetLoadMode in glint/plugin.go, so type information is guaranteed to be loaded.

    AST shape is acceptable when types are not load-bearing

    When the rule is fundamentally about the shape of code rather than its types, AST matching is fine. glint/no_anonymous_defer.go walks *ast.DeferStmt and asserts the call target is *ast.FuncLit:

    ins := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    
    ins.Preorder([]ast.Node{(*ast.DeferStmt)(nil)}, func(node ast.Node) {
        deferStmt := node.(*ast.DeferStmt)
    
        if _, ok := deferStmt.Call.Fun.(*ast.FuncLit); !ok {
            return
        }
    
        pass.ReportRangef(deferStmt, "%s", message)
    })
    

    There's no *types.Type that captures "anonymous deferred function" — the property only exists at the AST level — so AST matching is the right tool.

    Traversal: depend on inspect.Analyzer for deep walks

    When a rule needs to find nodes anywhere in the tree (calls, defers, type specs nested in functions, etc.), declare a dependency on the shared inspector rather than hand-rolling for _, file := range pass.Files { ast.Inspect(file, ...) }. inspect.Analyzer parses each file once and shares the resulting *inspector.Inspector across every dependent analyzer, so the package's analyzers walk each file once collectively instead of once per analyzer. Preorder also filters by node type up front, replacing the node.(*ast.T) type-switch-and-return true boilerplate:

    import (
        "go/ast"
    
        "golang.org/x/tools/go/analysis"
        "golang.org/x/tools/go/analysis/passes/inspect"
        "golang.org/x/tools/go/ast/inspector"
    )
    
    return &analysis.Analyzer{
        Name:     enforceO11yConventionsAnalyzer,
        Doc:      enforceO11yConventionsDefaultMessage,
        Requires: []*analysis.Analyzer{inspect.Analyzer},
        Run: func(pass *analysis.Pass) (any, error) {
            ins := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
    
            ins.Preorder([]ast.Node{(*ast.CallExpr)(nil)}, func(node ast.Node) {
                callExpr := node.(*ast.CallExpr)
                // ...
            })
    
            return nil, nil
        },
    }
    

    Notes when adopting it:

    • The callback returns nothing, so there is no return false to prune a subtree the way ast.Inspect allows. If a rule genuinely depends on pruning descent (matching a node and deliberately skipping its children, for example), keep a manual ast.Inspect and say why in a comment.
    • Preorder walks every file in the package with no file boundary, so any per-file scoping has to happen per node instead of around a pass.Files loop. When a rule applies only to certain files, filter inside the callback via pass.Fset.File(n.Pos()).Name(). For example, glint/no_testing_raw_sql.go checks for the _test.go suffix there because the test-only rule no longer has a file loop to gate.
    • Pulling the inspector from pass.ResultOf only works if the analyzer declared inspect.Analyzer in its Requires; otherwise the result is nil and the type assertion panics. This bites helpers shared across analyzers, since every caller must carry the dependency. For example, findAnnotatedStructs reads the inspector, so all four analyzers that call it list inspect.Analyzer in their Requires.

    Two situations where the manual walk is still the right call, and the inspector earns nothing:

    • Per-file stateful detection. When a rule accumulates state across a file and emits fixes scoped to that one *ast.File (an occurrence list, a "is this import used elsewhere" flag, import add/remove SuggestedFixes), the natural unit of work is the file, not the node. The shared inspector deliberately flattens that boundary, so adopting it would force re-bucketing nodes back by file to rebuild the same state, with no traversal saved. Keep the manual per-file ast.Inspect and leave a comment explaining the exemption. For example, glint/no_sql_err_no_rows.go collects each file's ErrNoRows occurrences alongside an otherSqlUsage flag before deciding which imports to rewrite.
    • Top-level-declaration scans. When a rule only cares about package-level declarations, iterating file.Decls directly is already a shallow single pass and expresses the intent precisely. Reaching for Preorder on *ast.GenDecl would additionally visit function-local declarations, widening the rule's scope, so the inspector is both unnecessary and subtly wrong here. For example, the audit-* analyzers (e.g. glint/audit_event_urn_naming.go) walk file.Decls to inspect only package-level *ast.GenDecl/*ast.TypeSpec.

    Source-string matching is a last resort

    Raw strings.Contains / regex over file content is fragile and almost never warranted. The only string-matching helper currently in the package is isSqlcGenerated in glint/no_repo_fields_in_service.go, and only because the "sqlc-generated" signal lives in a generated-file header comment that has no type-system representation:

    f, err := parser.ParseFile(token.NewFileSet(), pos.Filename, nil, parser.ParseComments|parser.PackageClauseOnly)
    if err == nil && ast.IsGenerated(f) {
        for _, cg := range f.Comments {
            if strings.Contains(cg.Text(), "sqlc") {
                result = true
                break
            }
        }
    }
    

    Note the helper caches results by filename, parses with parser.PackageClauseOnly to skip body parsing, and uses ast.IsGenerated to confirm the standard generated-code header before substring-matching. If you find yourself reaching for string matching, justify in a comment why type-based detection won't work — future readers will assume it was a last resort.

    Settings and ignore mechanisms

    Each analyzer accepts exactly one common setting: Disabled bool. Don't add allowlist fields (Ignored []string, regex includes/excludes, package globs, etc.) — they're a maintenance burden, they obscure the rule's contract, and golangci-lint already understands //nolint directives as the standard opt-out path.

    type noRepoFieldsInServiceSettings struct {
        Disabled bool `json:"disabled"`
    }
    

    The default opt-out for end users is the standard //nolint:glint directive (or //nolint:glint:<rule-key> for a specific rule). golangci-lint applies this without any analyzer-side wiring. If a particular violation is genuinely intentional in repo code, add //nolint:glint:<rule-key> with a brief comment explaining why.

    The only setting-shape exception today is narrow message customization: two analyzers (no-anonymous-defer, enforce-o11y-conventions) expose a Message string that gets appended to the default diagnostic when set. From glint/no_anonymous_defer.go:

    type noAnonymousDeferSettings struct {
        Disabled bool   `json:"disabled"`
        Message  string `json:"message"`
    }
    
    message := noAnonymousDeferDefaultMessage
    if rule.Message != "" {
        message += ": " + rule.Message
    }
    

    Add a Message field only if there's a concrete reason for end-user customization. Anything more elaborate than a single appended-suffix string warrants a discussion before being added.

    Diagnostic message style

    Diagnostics tell the reader what to do, not just what's wrong. Imperative present-tense, action-oriented, with the offending identifier inlined where it aids resolution. No trailing punctuation, no leading capital, match the tone of the existing analyzers.

    pass.ReportRangef(field, "Repo field detected in service struct.")
    

    Past-tense observation, capitalized, trailing period — describes the symptom but doesn't tell the reader what to change.

    pass.ReportRangef(field, "field %q in %s has type %s which is sqlc-generated; services should use *pgxpool.Pool and create repo instances in methods",
        field.Names[0].Name, s.name, fieldType)
    

    (from glint/no_repo_fields_in_service.go)

    Names the offending field with %q, explains why it's wrong, and tells the reader what shape the code should take instead.

    pass.ReportRangef(deferStmt, "%s", message) // message = "avoid anonymous deferred functions"
    

    (from glint/no_anonymous_defer.go)

    Imperative ("avoid"), short, no trailing punctuation.

    When the diagnostic has a mechanical fix, attach a SuggestedFix so editor quick-fix actions and golangci-lint --fix can apply it. See Suggested fixes.

    Analyzer.Doc

    Set the Doc field on every *analysis.Analyzer. It's what go vet -<rule> and IDE tooling show users when surfacing the rule. Keep it short and descriptive, and where the diagnostic has a single canonical default message, reuse the same constant for both so they stay in sync. From glint/no_sql_err_no_rows.go:

    const (
        noSqlErrNoRowsAnalyzer       = "nosqlerrnorows"
        noSqlErrNoRowsDefaultMessage = "use github.com/jackc/pgx/v5.ErrNoRows instead of database/sql.ErrNoRows"
    )
    
    return &analysis.Analyzer{
        Name: noSqlErrNoRowsAnalyzer,
        Doc:  noSqlErrNoRowsDefaultMessage,
        // ...
    }
    

    Suggested fixes

    When the diagnostic has a mechanical fix that's always safe to apply, attach SuggestedFixes to the analysis.Diagnostic so editor quick-fix actions and golangci-lint --fix can apply it. glint/no_sql_err_no_rows.go is the worked example:

    pass.Report(analysis.Diagnostic{
        Pos:      occ.Pos(),
        End:      occ.End(),
        Category: noSqlErrNoRowsAnalyzer,
        Message:  noSqlErrNoRowsDefaultMessage,
        SuggestedFixes: []analysis.SuggestedFix{{
            Message:   noSqlErrNoRowsFixMessage,
            TextEdits: edits,
        }},
    })
    

    Things to keep in mind when assembling TextEdits (upstream rules):

    • Each TextEdit applies to a single file; End must not be earlier in the file than Pos; for a pure insertion, set End equal to Pos (or token.NoPos). Edits within the same diagnostic must not overlap.
    • Build replacement text from AST nodes via go/format.Node rather than hand-concatenating strings, so the output stays gofmt-clean. For trivial selector or identifier replacements, raw byte literals are fine.
    • If the fix involves cross-occurrence work (e.g. removing an import once all call sites have been rewritten), attach the cross-cutting edits to every diagnostic's SuggestedFix. analysistest's three-way merge dedupes identical edits, but splitting them risks an editor quick-fix branch removing the import while sibling occurrences remain unfixed. See the explanatory comment in glint/no_sql_err_no_rows.go for the reasoning.

    Share editing helpers in subpackages

    Common edit logic belongs in a shared subpackage under glint/, not duplicated across analyzers. Today's example is glint/imports/, which exposes reusable helpers for emitting analysis.TextEdit values that add or remove imports:

    • LocalName(file, path, defaultName) (string, bool) — resolves the local identifier (alias or default) for an imported package, or reports that the import is absent.
    • Add(file, path) (analysis.TextEdit, bool) — emits an edit that inserts a new import into the file's first grouped import block.
    • Remove(fset, file, path) (analysis.TextEdit, bool) — emits an edit that deletes the import line for path.

    When you find yourself writing AST-rewriting helpers that another analyzer would plausibly need, add them under glint/<helper>/ with a package-level docstring describing the intended use in SuggestedFixes, and call them from your analyzer's Run function. Future analyzers should compose the helper rather than reimplement it.

    Testing fixes

    Test fix application with analysistest.RunWithSuggestedFixes instead of analysistest.Run. The harness applies the suggested fixes to the input file and compares the result against a <fixture>.go.golden file in the same fixture directory:

    func TestNoSqlErrNoRows(t *testing.T) {
        t.Parallel()
    
        testdata := analysistest.TestData()
        analysistest.RunWithSuggestedFixes(t, testdata, newNoSqlErrNoRowsAnalyzer(noSqlErrNoRowsSettings{}), "nosqlerrnorows")
    }
    

    Naming conventions

    There are three parallel namings to keep aligned for each analyzer: the rule key (user-facing, in YAML/JSON settings and //nolint directives), the Go identifiers (constant, settings struct, constructor), and the file name.

    Rule key (kebab-case, user-facing)

    • Prohibition rules use a no- prefix: no-anonymous-defer, no-repo-fields-in-service, no-sql-err-no-rows, no-direct-chat-message-insert.
    • Other rules use a descriptive predicate that reads naturally as an assertion about correct code: enforce-o11y-conventions, service-has-attach-func, service-has-service-assertion, audit-event-urn-naming, audit-event-typed-snapshot. Use a verb-led key when the rule is fundamentally about an action (enforce-...); use a <subject>-has-<property> or <subject>-<aspect> shape when the rule is an invariant check.

    Go identifiers (lowerCamel mirroring the rule key)

    The analyzer name constant, settings struct, and constructor mirror the rule key in lowerCamel form, with Go-style initialisms (SQL, URN, O11y):

    Rule keyConstantSettingsConstructor
    no-anonymous-defernoAnonymousDeferAnalyzernoAnonymousDeferSettingsnewNoAnonymousDeferAnalyzer
    no-repo-fields-in-servicenoRepoFieldsInServiceAnalyzernoRepoFieldsInServiceSettingsnewNoRepoFieldsInServiceAnalyzer
    no-sql-err-no-rowsnoSqlErrNoRowsAnalyzernoSqlErrNoRowsSettingsnewNoSqlErrNoRowsAnalyzer
    enforce-o11y-conventionsenforceO11yConventionsAnalyzerenforceO11yConventionsSettingsnewEnforceO11yConventionsAnalyzer
    service-has-attach-funcserviceHasAttachFuncAnalyzerserviceHasAttachFuncSettingsnewServiceHasAttachFuncAnalyzer
    audit-event-urn-namingauditEventURNNamingAnalyzerauditEventURNNamingSettingsnewAuditEventURNNamingAnalyzer

    The analyzer-name string constant is also the Name field of the returned *analysis.Analyzer, and it's the directory name analysistest looks for under glint/testdata/src/. Keep them identical so the test harness, the diagnostic source, and the rule key all line up.

    File names (snake_case mirroring the rule key)

    Each analyzer lives in glint/<rule-key-with-underscores>.go with a sibling _test.go:

    • no-anonymous-deferglint/no_anonymous_defer.go + glint/no_anonymous_defer_test.go
    • no-repo-fields-in-serviceglint/no_repo_fields_in_service.go + glint/no_repo_fields_in_service_test.go
    • audit-event-urn-namingglint/audit_event_urn_naming.go + glint/audit_event_urn_naming_test.go

    ruleSettings JSON tag

    In glint/plugin.go, the field on ruleSettings is UpperCamel and the json:"..." tag is the kebab-case rule key:

    NoRepoFieldsInService noRepoFieldsInServiceSettings `json:"no-repo-fields-in-service"`
    

    The JSON tag is what end users write under linters-settings.glint.rules.<key>.disabled in .golangci.yml, so it must match the rule key exactly.

    Testing

    Every analyzer ships with an analysistest-based test plus a fixture directory.

    Default-settings test

    Use golang.org/x/tools/go/analysis/analysistest and pass the constructor with zero-value settings. The fourth argument is the directory name analysistest looks up under glint/testdata/src/:

    func TestNoAnonymousDefer(t *testing.T) {
        t.Parallel()
    
        testdata := analysistest.TestData()
        analysistest.Run(t, testdata, newNoAnonymousDeferAnalyzer(noAnonymousDeferSettings{}), "noanonymousdefer")
    }
    

    Fixtures

    Fixtures live at glint/testdata/src/<fixture-name>/<file>.go. Mark expected diagnostics with // want "<regex>" comments on the offending line:

    package noanonymousdefer
    
    func bad() {
        defer func() {}() // want "avoid anonymous deferred functions"
    }
    

    For diagnostics whose text contains regex metacharacters (*, (, ., etc.), escape them in the // want directive and use backticks instead of double quotes when the message itself contains quotes:

    repo *repo.Queries // want `field "repo" in Service has type \*serviceannotationrepofield/repo.Queries which is sqlc-generated; services should use \*pgxpool.Pool and create repo instances in methods`
    

    Non-default settings

    For each non-default setting variant (e.g. a custom Message), add a separate test function and a separate fixture directory whose // want text reflects the customized output:

    func TestNoAnonymousDeferCustomMessage(t *testing.T) {
        t.Parallel()
    
        testdata := analysistest.TestData()
        analysistest.Run(
            t,
            testdata,
            newNoAnonymousDeferAnalyzer(noAnonymousDeferSettings{Message: "use a named deferred helper instead"}),
            "noanonymousdefercustommessage",
        )
    }
    

    Disabled-rule path

    The disabled-rule test surface is centralized in glint/plugin_test.go, which exposes a disabledAllRulesPlugin() helper returning a *plugin with every rule's Disabled: true. Two assertions live there:

    • TestBuildAnalyzersAllDisabledBuildAnalyzers returns an empty slice when everything is disabled.
    • TestBuildAnalyzersAllEnabled — asserts the expected analyzer count for a zero-value *plugin{}. Bump this number whenever a new analyzer is added.

    Each analyzer's own test file then adds a per-rule registration test: start from disabledAllRulesPlugin(), flip only the rule under test back on, and assert BuildAnalyzers returns exactly that one analyzer with the expected name. From glint/no_repo_fields_in_service_test.go:

    func TestBuildAnalyzersSkipsDisabledNoRepoFieldsInService(t *testing.T) {
        t.Parallel()
    
        p := disabledAllRulesPlugin()
        p.settings.Rules.NoRepoFieldsInService.Disabled = false
    
        analyzers, err := p.BuildAnalyzers()
        require.NoError(t, err)
        require.Len(t, analyzers, 1)
        require.Equal(t, noRepoFieldsInServiceAnalyzer, analyzers[0].Name)
    }
    

    When adding a new analyzer, extend disabledAllRulesPlugin() with the new Disabled: true field, increment the expected count in TestBuildAnalyzersAllEnabled, and add the per-rule registration test alongside the analyzer's own unit tests.

    Frequently asked questions

    What to verify before installation and use

    What does the glint source document cover?

    When adding a new analyzer:

    How do I install glint?

    The source record exposes this install command: npx skills add https://github.com/speakeasy-api/gram --skill ".agents/skills/glint". Inspect the command and pinned source before running it.

    Which permission-related actions were detected?

    Static rules flagged read-files, write-files in the source; the page lists the matching lines and excerpts.

    Alternatives

    Compare before choosing

    Computed 10045,511

    coreyhaines31/marketingskills

    ab-testing

    When the user wants to plan, design, or implement an A/B test or experiment, or build a growth experimentation program. Also use when the user mentions "A/B test," "split test," "experiment," "test this change," "variant copy," "multivariate test," "hypothesis," "should I test this," "which version is better," "test two versions," "statistical significance," "how long should I run this test," "growth experiments," "experiment velocity," "experiment backlog," "ICE score," "experimentation program

    Computed 10024,921

    alirezarezvani/claude-skills

    app-store-optimization

    App Store Optimization (ASO) toolkit for researching keywords, analyzing competitor rankings, generating metadata suggestions, and improving app visibility on Apple App Store and Google Play Store. Use when the user asks about ASO, app store rankings, app metadata, app titles and descriptions, app store listings, app visibility, or mobile app marketing on iOS or Android. Supports keyword research and scoring, competitor keyword analysis, metadata optimization, A/B test planning, launch checklist

    Computed 100152

    JasonColapietro/suede-creator-skills

    suede-ab-testing

    Suede-owned experimentation discipline for hypotheses, sample sizing, test duration, significance, and repeatable experiment programs. Use when comparing variants, deciding whether a result is reliable, or building an experiment backlog and cadence. NOT FOR: analytics instrumentation (use suede-analytics), post-click conversion diagnosis (use suede-site-alchemy), or writing the variant copy itself (use suede-copy).

    Computed 10035

    tenequm/skills

    founder-playbook

    Decision validation and thinking frameworks for startup founders. Use when you need to pressure-test a decision, validate your next steps, think through strategic options, or sanity-check your approach. Triggers on phrases like "should I", "help me think through", "is this the right move", "validate my thinking", "what am I missing". Covers fundraising, customer development, runway management, prioritization, and crypto/web3 founder challenges.