A Go toolchain is required. The project uses mise to manage it (mise.toml).
mise installIf go is not on PATH, resolve it through mise:
export PATH="$(mise where go)/bin:$PATH"go build -o bin/sah ./cmd/sahFor release builds, inject the version via ldflags.
go build -trimpath -ldflags="-s -w -X main.version=$(cat VERSION)" -o bin/sah ./cmd/sahRun from the project root.
sah auth login
sah run
sah me
sah daemon installsah daemon install both installs and starts the per-user background service. Use sah daemon start only after a manual stop or after logging back into the relevant service manager session.
On Linux, sah daemon install writes a per-user systemd --user unit and restarts it immediately. On macOS, it writes a per-user launchd plist and bootstraps it immediately. Unless you pass --agent, --agents, or --rotate-installed, the install command detects every installed supported agent CLI and persists round-robin mode for the daemon automatically.
If you want the Linux user service to keep running without an active login session, enable lingering first:
loginctl enable-linger "$USER"If the daemon cannot find codex, gemini, claude, or qwen, sah daemon install fails before it starts the service. Run sah agents to inspect detection, then re-run the install from a shell where at least one supported CLI is already on PATH. The install command captures the current shell environment for the background service manager, stores absolute agent binary paths, and runs the daemon from the saved config directory instead of the shell's working directory.
sah auth login prints a verification URL and short code. Open the URL in any browser, sign in if needed, enter the code, and the CLI will finish automatically once sign-in is approved.
go test ./...Detailed test-layer and quality-gate guidance lives in testing.md.
Coverage is enforced through the shared helper script used by pre-commit and CI.
./scripts/check_coverage.sh 35golangci-lint runThe lint profile also enforces:
- cognitive complexity (
gocognit) - function length (
funlen) - duplicate blocks (
dupl)
To match the Linux CI target from macOS, also run:
GOOS=linux GOARCH=amd64 golangci-lint run ./...The repository includes a pre-commit hook in .githooks/. Enable it once after cloning:
git config core.hooksPath .githooksThe hook runs:
go mod verifygo mod tidyand checks that it does not rewritego.modorgo.sumCGO_ENABLED=1 go test -race ./..../scripts/check_coverage.sh 35golangci-lint config verifygolangci-lint run ./...GOOS=linux GOARCH=amd64 golangci-lint run ./...go build -o .tmp-bin/sah ./cmd/sah
main now treats VERSION as the release source of truth.
When VERSION changes on main, GitHub Actions (.github/workflows/tag-release.yml) creates the corresponding annotated v* tag automatically and then dispatches the existing release workflow (.github/workflows/release.yml) for that tag. The release workflow runs GoReleaser, builds macOS and Linux binaries, creates archives with checksums, publishes a GitHub Release, and updates the Homebrew tap by writing the formula into the tap repository's Formula/ directory.
Typical release flow:
printf 'v0.8.0\n' > VERSION
git add VERSION
git commit -m "Release v0.8.0"
git push origin mainManual tags are no longer the normal path. If VERSION already has a matching tag on the current commit, the tag workflow exits without doing anything. If the tag already exists on a different commit, the workflow fails instead of silently reusing the old release.
Required repository secrets:
HOMEBREW_TAP_TOKEN: token with write access tocorca-ai/homebrew-tap
Configuration is in .goreleaser.yaml.