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-orSRE-). A skipped premise hides a regression, so fail it witht.Fatalfinstead. This is enforced byscripts/skip-reason-guard.py. - Gap-driven changes should reference the relevant
*GAP_ANALYSIS.mdID in the commit message, for examplefix: 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.pyis a hard deny: every otherRename,LinkorMoveFileExfails unless it is a reviewed entry inscripts/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 security@devnw.com 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.