Sonny is proprietary commercial software, not an open-source project. This page is for developers with CodePros-authorized source access. See Licensing and access .

Sonny is a Go project. Everything below is the contributor contract enforced by the repository’s own CI jobs and Git hooks, so a change that passes locally has a good chance of passing review.

The canonical source for this page is CONTRIBUTING.md at the root of the repository. If the two ever disagree, the repository file wins.

Source access

Repositories, issue tracking, and release assets are private. Use the access provided to your organization by CodePros.

The command-line engine lives in cmd/sonny/cli, the terminal UI in cmd/sonny/ui, and the distributed mesh in mesh/. The command surface is documented on the CLI reference and the terminal behaviour in the getting started guide .

Prerequisites

The module declares go 1.27.3 in go.mod. Use that toolchain or newer — the lint and hook setup installs the same pinned version and toolchain as CI rather than falling back to whatever binary is on your PATH.

Lint runs through a pinned golangci-lint v2.14.0, installed into ./bin by the make targets. Keep bin/ between runs and share the normal Go and golangci-lint caches across worktrees.

Build, test, lint

# Build both binaries
go build ./...

# Quick unit tests
go test -short -timeout=90m ./...

# Full test suite (race + cover)
go test -race -coverprofile=coverage.out ./...

# Lint with the same pinned golangci-lint and .golangci.yml as CI's go-lint job,
# plus the nolint, complexity and skip-guard scripts
make lint

# Apply the formatters configured in .golangci.yml via golangci-lint fmt
make fmt

# Lint the build-tag and GOOS variants the default lint never loads
# (integration/e2e/desktop/prod/pam tags, *_windows.go, *_darwin.go)
make lint-variants

# Check formatting without rewriting anything
make fmt-check

make lint adds --timeout 75m --concurrency 2 in CI and reports only issues new since the repository’s baseline revision, so a local run also surfaces older debt that CI will not fail you on. make fmt-check by default covers only the files the Linux lint never loads (the //go:build !linux files and the _windows.go / _darwin.go variants). Set FMT_CHECK_BASE=<ref> to add the files changed since that ref — that is what CI’s go-fmt-check job runs — or FMT_CHECK_ALL=1 to check the whole tree.

Run make lint, make fmt and make fmt-check before opening a pull request. CI does not run those targets verbatim: its go-lint job applies the same linter and config as a new-issue ratchet, go-fmt-check runs make fmt-check FMT_CHECK_BASE=<diff base>, and the guard scripts run as their own jobs.

How much local lint covers

The local hook (including govet) checks the packages containing your changed Go files, and includes staged deletions. Deleting a whole package, changing module/workspace dependencies, or changing shared linter or hook settings runs the full root module. Nested modules stay outside the root-module check; CI still does the full-module lint. A package with no files in the default Linux build (Windows-only, desktop-tagged) is skipped locally — make lint-variants covers those.

Repository build entry points have a hard ceiling of 12 workers. Memory-heavy checks stay lower: the default linter uses four package workers and one gosec worker per package, while CI lint and race suites use two. Explicit GOMEMLIMIT and GOMAXPROCS values below the 12-worker ceiling take precedence; larger concurrency values are clamped. A cold run still analyses the complete dependency graph; subsequent unchanged runs reuse that work.

To diagnose a slow lint run, use the pinned binary with profiling:

bin/golangci-lint run -v --cpu-profile-path /tmp/sonny-lint.cpu ./...
go tool pprof -top /tmp/sonny-lint.cpu

The verbose log separates package loading from analyzer time. Set GL_DEBUG=goanalysis/issues/cache to see which packages miss the analysis cache.

Pull request rules

  • One concern per PR. If you find yourself touching unrelated files, split it.
  • New behaviour requires a test that fails before the change and passes after. No mocks.
  • A test may skip only when its environment genuinely cannot run it — a missing tool, testing.Short(), an unsupported OS/arch, or a missing credential/service — and the skip message must say which. Any other skip names a ticket (SONNY-, ENG-, SEC- or SRE-). A skipped premise hides a regression, so fail it with t.Fatalf instead. This is enforced by scripts/skip-reason-guard.py.
  • Gap-driven changes should reference the relevant *GAP_ANALYSIS.md ID in the commit message, for example fix: close Q-007 - add .golangci.yml.
  • Generated files (gen/**/*.gen.go, mesh/proto/**/*.pb.go) are regenerated, never hand-edited. If the regeneration pipeline breaks, surface that as a separate gap.
  • Publish whole files with internal/atomicfile (Write / WriteNew) — not a hand-rolled temp file plus rename or link. scripts/atomic-write-guard.py is a hard deny: every other Rename, Link or MoveFileEx fails unless it is a reviewed entry in scripts/atomic-write-guard.allow.json, which is hand-edited; the guard never adds an entry itself.

Test expectations

The test rules above are the short version. The practical consequence when you add a command or fix a bug: write the failing test first, keep it mock-free, and if it needs an external service, say which one in the skip message so the guard can tell an environment gap from a hidden regression. The troubleshooting page covers the failures people hit most often while running the suite locally.

Commit message style

Every non-merge commit must use a Conventional Commit header and contain exactly one external work-item key. Put the key in the subject or in a Refs: footer:

fix(parser): reject malformed input (ENG-1234)
feat(mesh): add peer discovery

Refs: ENG-1234

This also applies to direct pushes to main: CI validates each commit in the immutable pushed range, so a later commit cannot repair an earlier message that omitted its key. Merge commits are exempt; generated revert subjects still require one key. The local commit-msg hook and the precommitci-authoritative CI job enforce the same policy, and the pre-commit commitmsg-policy entry runs go run ./cmd/commitmsgcheck, a small adapter over the shared validator.

Body text is optional and should explain the why, not the what.

Bot footers are not this repo’s convention — omit them. That applies to AI-assisted commits and to changes produced by delegated workers. Do not add a model name or an assistant identity as a co-author, and do not restore an omitted bot footer during an amend or cherry-pick. Preserve the real contributor’s Git author and committer metadata, and record useful tool or model provenance in the work-item evidence or the review description instead of inventing contributor identities. The validator checks Conventional Commit structure and work-item identity; it neither requires nor validates bot attribution, and passing it does not override this convention.

Identifiers

New identifiers are UUIDv7 from github.com/gofrs/uuid/v5, always through internal/uuidx (uuidx.New(), uuidx.NewString()). Do not import github.com/google/uuid or hand-roll IDs from crypto/rand hex. uuidx.Short(n) exists for short non-UUID tokens. .golangci.yml blocks google/uuid through gomodguard, and internal/uuidx’s TestNoGoogleUUIDImporters fails if any package imports it.

User-visible capability parity

Any change that adds, removes, renames, reorders, or changes the arguments of a TUI DefaultCommands() entry must update the canonical row in registry.d/. The same rule applies when the desktop client gains or loses a native route for an existing command capability. The registry package owns command identity, lifecycle, desktop disposition, gap ownership, and per-surface evidence — do not copy that data into another registry.

Complete daemon/client workflows are grouped separately in supervisor/parity/workflows.json. Its command_registry_ids are foreign keys into registry.d, while the workflow contract owns daemon method families, shared client sources, desktop presentation and host actions, state profiles, safety, and recovery behaviour. Run go test ./supervisor/parity after changing the workflow contract, and go test ./registry ./supervisor/parity after changing either contract. The tests reject an unassigned or multiply assigned command row, an unknown command reference, or drift across the daemon, shared-client, and desktop-host inventories.

Releasing

The release procedure lives in docs/release/RELEASING.md: preparing the .release-version commit, gating the exact head with make release-gate SHA=<sha> before tagging, what the tag pipeline actually gates on, and why a pushed tag is bumped and re-cut rather than moved. Read it before you tag anything — a red tag pipeline costs a version number permanently. Release binaries and install paths are described on the install page .

Reporting a security issue

If you believe you have found a security issue, do not open a public issue. Email [email protected] as described in SECURITY.md. The trust model for Sonny’s tool surface — including which MCP servers and shell commands need approval — is documented on the MCP trust model page.

Code of conduct

Be kind. Disagree on the technical merit, not the person.