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:
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
| Platform | Status | Evidence | What to check |
|---|---|---|---|
| Codex | Not declared | No explicit evidence | Portability before use |
| Claude Code | Not declared | No explicit evidence | Portability before use |
| Cursor | Not declared | No explicit evidence | Portability before use |
| Gemini CLI | Not declared | No explicit evidence | Portability before use |
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.
npx skills add https://github.com/speakeasy-api/gram --skill ".agents/skills/glint"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
- 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. - 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… - 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… - 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. - 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
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
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
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
| Signal | Value | Evidence type | Meaning |
|---|---|---|---|
| Quality score | 91/100 | Computed | Documentation, specificity, maintenance, and trust rules |
| Repository stars | 266 | Source | Repository attention, not individual Skill quality |
| Compatibility | 0 platforms | Source | Declared in the catalog source record |
| Usage guide | automated source guide | Editorial | Generated 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 thesettings/ruleSettingsstructs, and lists every analyzer inBuildAnalyzers. - gcl wiring: server/.custom-gcl.yml — declares
github.com/speakeasy-api/gram/glintas 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:
- Pick a kebab-case rule key and a matching
snake_case.gofile name inglint/. See Naming conventions. - Define
<rule>SettingswithDisabled bool. Add nothing else unless there's a concrete user-facing reason. See Settings and ignore mechanisms. - 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. - Write the diagnostic with imperative tone and the offending identifier inlined via
%q. See Diagnostic message style. - Wire the rule into glint/plugin.go: add a
ruleSettingsfield with the kebab-case JSON tag, then anif !p.settings.Rules.<X>.Disabledblock inBuildAnalyzers. - Add
<rule>_test.gowithanalysistest.Run(...)and a fixture underglint/testdata/src/<fixture>/. UpdatedisabledAllRulesPlugin()and the count inTestBuildAnalyzersAllEnabledin 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 falseto prune a subtree the wayast.Inspectallows. If a rule genuinely depends on pruning descent (matching a node and deliberately skipping its children, for example), keep a manualast.Inspectand say why in a comment. Preorderwalks every file in the package with no file boundary, so any per-file scoping has to happen per node instead of around apass.Filesloop. When a rule applies only to certain files, filter inside the callback viapass.Fset.File(n.Pos()).Name(). For example, glint/no_testing_raw_sql.go checks for the_test.gosuffix there because the test-only rule no longer has a file loop to gate.- Pulling the inspector from
pass.ResultOfonly works if the analyzer declaredinspect.Analyzerin itsRequires; 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,findAnnotatedStructsreads the inspector, so all four analyzers that call it listinspect.Analyzerin theirRequires.
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/removeSuggestedFixes), 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-fileast.Inspectand leave a comment explaining the exemption. For example, glint/no_sql_err_no_rows.go collects each file'sErrNoRowsoccurrences alongside anotherSqlUsageflag before deciding which imports to rewrite. - Top-level-declaration scans. When a rule only cares about package-level declarations, iterating
file.Declsdirectly is already a shallow single pass and expresses the intent precisely. Reaching forPreorderon*ast.GenDeclwould additionally visit function-local declarations, widening the rule's scope, so the inspector is both unnecessary and subtly wrong here. For example, theaudit-*analyzers (e.g. glint/audit_event_urn_naming.go) walkfile.Declsto 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
TextEditapplies to a single file;Endmust not be earlier in the file thanPos; for a pure insertion, setEndequal toPos(ortoken.NoPos). Edits within the same diagnostic must not overlap. - Build replacement text from AST nodes via
go/format.Noderather 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 forpath.
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 key | Constant | Settings | Constructor |
|---|---|---|---|
no-anonymous-defer | noAnonymousDeferAnalyzer | noAnonymousDeferSettings | newNoAnonymousDeferAnalyzer |
no-repo-fields-in-service | noRepoFieldsInServiceAnalyzer | noRepoFieldsInServiceSettings | newNoRepoFieldsInServiceAnalyzer |
no-sql-err-no-rows | noSqlErrNoRowsAnalyzer | noSqlErrNoRowsSettings | newNoSqlErrNoRowsAnalyzer |
enforce-o11y-conventions | enforceO11yConventionsAnalyzer | enforceO11yConventionsSettings | newEnforceO11yConventionsAnalyzer |
service-has-attach-func | serviceHasAttachFuncAnalyzer | serviceHasAttachFuncSettings | newServiceHasAttachFuncAnalyzer |
audit-event-urn-naming | auditEventURNNamingAnalyzer | auditEventURNNamingSettings | newAuditEventURNNamingAnalyzer |
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-defer→glint/no_anonymous_defer.go+glint/no_anonymous_defer_test.gono-repo-fields-in-service→glint/no_repo_fields_in_service.go+glint/no_repo_fields_in_service_test.goaudit-event-urn-naming→glint/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:
TestBuildAnalyzersAllDisabled—BuildAnalyzersreturns 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
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
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
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).
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.