Guidance for AI agents and human contributors working in the go-application-framework (GAF). Read this first; it sets the ground rules, then points you at the deeper docs.
GAF is a framework / shared library, not an application. It is consumed as a dependency by downstream projects — most notably the Snyk CLI — and it must serve many different kinds of consumers at once: the CLI today, and IDE integrations, MCP servers, and other host applications tomorrow.
That single fact drives everything below:
- You are writing API, not app code. Every exported symbol in
pkg/is part of a contract that other teams build on. Code that would be fine inside one application can be a breaking change here. - Stability is a feature. Downstream consumers pin to GAF via a
go.modreplace/version. Changing or removing an exported signature, struct field, configuration key, content type, or workflow identifier can break them without warning. Treat the public surface as load-bearing. - Cater to multiple application types. Don't bake in CLI-only assumptions
(e.g. "there is a terminal", "output goes to stdout", "the user can be
prompted"). Drive behavior through
Configuration,InvocationContext, and content types so an IDE or server consumer can wire it differently. - Self-description over hard-coding. Workflows and data describe themselves (identifiers, config options, content types, metadata) so generic consumers can discover, inspect, and route them without per-workflow glue. Invest in that metadata. See the architecture guide.
Before changing anything under pkg/, ask:
- Am I changing an exported function/method/type/field signature? → breaking.
- Am I removing or renaming a configuration key, workflow identifier, or content type? → breaking for whoever depends on it.
- Am I changing default behavior that a consumer may rely on? → breaking.
- Can I make this additive instead (new function, new optional field, new functional option, new default value) and keep the old path working? → prefer this.
- If a break is truly unavoidable, it must be a
feat!/fix!or carry aBREAKING CHANGE:footer so semantic-release bumps the major version. See CONTRIBUTING.md.
GAF's source, full git history, issues, and PRs are publicly visible on GitHub, regardless of the fact that contributions currently come from Snyk employees only (see CONTRIBUTING.md). Anything you commit is exposed externally forever, even if a later commit reverts it.
Never include, in code, comments, docs, commit messages, or PR descriptions:
- Names, URLs, or code copied/paraphrased from Snyk-internal or other private repositories.
- Internal hostnames, service names, API endpoints, or architecture details not already public.
- Contents of internal tickets, Slack threads, or wiki/Confluence pages beyond
the bare
[XXX-XXXX]ticket ID token already used in commit messages. - Customer, partner, or employee names not relevant to an external contributor.
- Credentials, tokens, or other secrets (even test/dummy-looking ones pulled from an internal fixture).
If a task's context comes from an internal source, restate the requirement in your own words for anything that lands in this repo — do not paste or closely mirror the source material.
pkg/ ← Public API surface (exported; treat as load-bearing)
workflow/ Engine, WorkflowIdentifier, Data
configuration/ Configuration interface
app/ CreateAppEngine entry point
networking/ HTTP client with auth/proxy support
auth/ Authentication helpers
analytics/ Analytics pipeline
instrumentation/ Instrumentation hooks
logging/ Structured logging
ui/ User-facing output (not terminal-specific)
local_workflows/ Built-in workflows + output pipeline
apiclients/ Generated API clients
utils/ Utilities including UFM (Unified Finding Model)
mocks/ Shared mock implementations
envvars/ Environment-variable constants
configtest/ Test helpers for Configuration
devtools/ Developer tooling utilities
runtimeinfo/ Runtime information helpers
internal/ ← Private implementation details (not part of the public API)
api/ Internal API helpers
constants/ Internal constants
presenters/ Output formatters
ufm_helpers/ UFM processing internals
local_findings/ Local finding sources
utils/ Internal utilities
mocks/ Internal mocks
cmd/
ufm-fixture-tool/ CLI tool for redacting UFM test fixtures
docs/ Architecture guide and design docs
scripts/ Linting, fixture generation, API spec scripts
Tests live next to their source files (foo.go → foo_test.go).
- The workflow callback is a thin integration shell — extract services from
InvocationContext, read config, call domain logic, package[]Data. No business logic in the callback. - Domain code has no GAF imports — pass concrete types (
context.Context,*http.Client, config values), neverInvocationContext, into domain logic. - Structure:
init.go(register + config defaults) ↔workflow.go(thin callback) ↔domain/(pure logic). - Describe yourself at registration — meaningful identifiers, flags, usage strings, content types. They become CLI help, IDE tooltips, and MCP descriptions.
- Route by content type, wrap errors with
%w, use error-catalog types for user-facing problems, clone config before cross-workflow invocation.
- Plan first. For non-trivial changes, save an implementation plan under
${issueID}_implementation_plan/(issueID is theXXX-XXXXtoken in the branch name) and get it reviewed by the user before editing production code. Do not commit the plan to the final PR. - Minimal, on-target changes. Don't refactor or optimize beyond the goal. Match existing patterns and conventions.
- Tests always. Write/update unit tests AND integration tests and iterate until they pass. Use
t.Setenv(neveros.Setenv). Use existinggomock-generated mocks; don't hand-roll new ones. Don't fix test data to make tests pass. - Run the tooling before committing:
make format,make lint,make test,make generate. Then verifygit diff --name-onlyis clean (stage anything the tools changed). - Security: run
snyk code test <absolute-project-path>after code edits andsnyk test <absolute-project-path>aftergo.modchanges. Fix fixable findings (not in test data). Before pushing, also re-read your diff specifically for internal-only repo names, hostnames, ticket contents, or other non-public references (see This repository is public) — Snyk Code and secrets scanning will not catch these. - Commits: Conventional Commits
(
type: summary); subject under 72 chars + descriptive body; append[XXX-XXXX]from the branch name — the bare ID only; do not paste the internal ticket's title or description verbatim, as it may contain internal-only context. Each commit must stand on its own without breaking the release pipeline. Never force-push; always confirm with the user before pushing.
We follow the Clean Code Developer value system. Its grades are a cumulative ladder; below we list the code-design principles of each grade with a concrete GAF example. These are the ones that directly shape a stable, multi-consumer framework. Most are demonstrated in full in the Extension Architecture Guide — follow the links for the worked examples.
- DRY — a fact lives in one place; e.g. define a config key or content type as a single exported constant, not a string literal repeated across callbacks.
- KISS — prefer the straightforward workflow over a clever one; a thin callback calling a plain domain function beats premature abstraction.
- Favour Composition over Inheritance — compose behavior via interfaces and
functional options (
WithConfig,WithInput) rather than type hierarchies. - IOSP (Integration/Operation Segregation) — a function either integrates (composes calls) or operates (does logic), never both; this is the thin-callback rule.
- Single Responsibility (SRP) — a workflow does one thing; split
ignore.create/ignore.editrather than overloading one callback. - Separation of Concerns — keep registration (
init.go), wiring (workflow.go), and logic (domain/) in distinct layers. - Single Level of Abstraction — don't mix high-level orchestration and byte-fiddling in the same function.
- Dependency Inversion (DIP) —
domain/depends oncontext.Context,*http.Client, and config values — never onInvocationContext(see Testability and the Anti-Patterns). - Interface Segregation (ISP) — expose small, focused interfaces so consumers don't take a dependency on methods they don't use.
- Information Hiding — keep implementation in
internal/; expose only what the public contract needs. - Least Astonishment — a load-bearing stability rule: consumers must not be surprised by a changed signature, default, or behavior.
- Open/Closed (OCP) — the framework's north star: extend via new workflows/extensions and functional options, don't modify existing public behavior.
- Tell, Don't Ask — hand work to a type rather than pulling out its state and acting on it externally.
- Law of Demeter — avoid reaching through object graphs
(
a.GetB().GetC().Do()); keep interactions shallow.
- YAGNI — don't add speculative public API you'd then have to keep stable forever; add it when a consumer actually needs it.
- Component Orientation — the engine-plus-extensions (microkernel) model; build features as self-describing, independently-contractable components (see The Microservice Analogy).
- Implementation Reflects Design — the package layout should physically mirror the intended architecture.
Each grade also carries practices (version control, reviews, automated unit/integration tests, CI,
make lint, gomock test doubles, conventional commits) — these live in Working agreements and the Task execution checklist above rather than being repeated here.
Use these lenses when evaluating a design proposal, implementation plan, or PR.
- Not everything in GAF is the same. While the GAF repo is a common home for source code, there are different layers/aspects implemented:
internal/is for internal implementation detailspkg/is the public APIpkg/workflow/implements the workflow engine, which is the extensibility/orchestration layer for local clients. To extensions authors it provides an opinionated usage of the basic library functions like configuration, logging, network, ... This way authors can use functions consistently and don't have to deal with the complexity of the basic library functions but can focus on the actual business logic.pkg/appis the application integration layer, it provides a pre-configured workflow.Engine, whose configuration can be extended with application specific aspects by the final applications like the CLI ...pkg/local_workflows/hosts extension workflows available for all applications and are based on theworkflowpackagepkg/apiclientscontains auto-generated API clients from Open API Specs, in addition it might also contain a higher level API abstraction if reasonablepkg/*are raw/basic library functions that might depend on other gaf library functions but basically have no assumptions on how they are used and should not know about products etc
docs/is for documentation.
- Default to
internal/. A type, function, or constant goes inpkg/only when a consumer outside this repo genuinely needs it today. Moving frominternal/→pkg/is easy; the reverse is a breaking change. - Prefer functional options pattern. For internal wiring between framework components,
functional options (
With…) keep the surface narrow and typed. - Keep related code close to each other. Don't spread related code across different packages. For example, keep configuration key constants close to the code that uses them.
Red flags that a design needs simplification:
- Exporting "just in case." If no consumer uses it today, keep it internal. Exported API is a forever-commitment in a library.
- Many code changes for a seemingly simple functionality. Keep changes and code impact to a minimum. Boy Scout Rule and refactorings are good but need to balance advantages with disadvantages. Always question if a change is necessary to achieve the goal; if you keep answering multiple times with no, consider doing these changes in a later increment. This helps to improve the review and reduces risk, overall getting things merged faster.
Every design decision must pass the "works for CLI, IDE, and MCP" test:
- No terminal assumptions. Don't assume stdout, stderr, or an interactive
prompt exists. Use
InvocationContext's UI abstraction for user-facing output andDatacontent types for structured results. - No single-invocation assumptions. An MCP or IDE host may invoke many
workflows in the same process. Avoid package-level mutable state; prefer
per-invocation state threaded via
InvocationContextorcontext.Context. - Configuration, not hard-coding. Behavior that might differ by consumer
belongs in
Configurationwith a sensible default — not in a constant. If a CLI needs a flag and an IDE needs a settings key, both read the same config entry. - Use Configuration, not Environment Variables. Configuration abstracts the source of data and the usage of data. This enables writing workflows that use data without knowing where it comes from. The applications can wire up the configuration to different sources like environment variables, command line flags, configuration files, etc.
- Use Configuration, not API calls directly. For example when accessing remote feature flags in code, access them through the configuration
rather than an API client directly. The Configuration package brings lazy loading and caching and there are helpers for feature flags in
pkg/local_workflows/config_utils/feature flag names should live close to the business logic. - One Configuration value can come from multiple sources. use configuration.AlternativeKeys() to register a precedence of different sources to look up a value from for example in a CLI application from cmd arguments, environment variables, config files, remote configuration ...
- Configuration DefaultFunctions are multi purpose helper. even if called default function, they can be used to provide a default value for a configuration key but also perform validation and translation logic
- Errors in Data vs. returned errors. Use
data.AddError()for non-fatal issues alongside valid output (e.g. partial scan results). Return anerrorwhen the workflow cannot produce any meaningful output.
- Test public API. Test need to cover the public API.
- Test coverage. test internal APIs, unexported functions and dependency injection to increase test coverage for edge cases.
Requires Go 1.26+. Install dev tools:
make tools # installs golangci-lint v2.10.1 into .bin/When you receive a task, follow this sequence:
- Orient — identify affected packages; check if they are in
pkg/(public) orinternal/(private). If public, apply the stability checklist. - Plan — for non-trivial work, draft an implementation plan and confirm with the user.
- Write/update tests — before or alongside the implementation.
- Implement — keep the change minimal and on-target.
- Verify — run
make format && make lint && make test && make generate. - Check for drift —
git diff --name-only; stage anything the tools changed. - Commit — conventional-commit format, descriptive body.
make help # list all targets
make tools # install golangci-lint
make format # gofmt
make lint # golangci-lint
make test # unit tests with race detector
make generate # regenerate mocks and API clientsLocal development against a consumer (e.g. the CLI) — add to the consumer's go.mod:
replace github.com/snyk/go-application-framework => ../../go-application-framework
