Contributing to amux

July 12, 2026 · View on GitHub

Development

git clone https://github.com/andyrewlee/amux.git
cd amux
./scripts/install-hooks.sh
make lint-tools   # one-time: builds the pinned golangci-lint into ./.cache/bin
make run

Run make lint-tools once before your first make devcheck or git commit. It builds the linter version pinned in .golangci-version into the gitignored ./.cache/bin; a stock golangci-lint from PATH may be a different version from CI and produce different diagnostics. See LINTING.md for the full rationale.

The minimum supported Go family is 1.26 (the go directive in go.mod). The toolchain directive in go.mod pins the patched Go 1.26 toolchain used for local checks, CI, and releases. With the standard GOTOOLCHAIN=auto setting, the go command switches to that patched toolchain automatically. If you force GOTOOLCHAIN=local, install the pinned patch release yourself before running repo checks.

Run the fast local checks:

make devcheck

make devcheck is the required pre-PR gate: it runs vet, tests, and lint (including file-length checks). It is the fast subset of CI, not the whole of it — CI additionally enforces the race detector and go mod tidy cleanliness. For the full local CI mirror, run:

make ci

make ci runs devcheck plus make test-race (CI's race gate; slow), make tidy-check (CI's tidy gate), and make govulncheck (CI's vulnerability scan, using the same pinned govulncheck version — it also works standalone to reproduce a CI vuln failure). CI's harness smoke steps are not mirrored locally.

For the inner loop, launch the TUI with make run in a real terminal — amux requires stdin, stdout, and stderr to all be TTYs, so it only runs directly in your terminal. air cannot host the TUI: it launches the rebuilt binary with stdin on /dev/null, which fails that TTY check, so make dev is not a hot-reload TUI loop. Use it instead for automatic rebuilds and compile-error feedback while you edit — run make dev in a second pane alongside make run. It runs air with the repo's .air.toml and rebuilds on save. Install it once with:

go install github.com/air-verse/air@latest

Ensure $(go env GOPATH)/bin is on your PATH; otherwise make dev prints this same install hint and exits.

For style-only cleanup, run:

make fmt

Before opening larger PRs, also run strict ratcheted lint on changed code:

make lint-strict-new

Pull requests are CI-gated (automated). For local confidence before opening a PR:

  • always: make devcheck, make lint-strict-new
  • if touching concurrency (supervisor workers, PTY read loops, watchers, activity leases, anything with goroutines/channels/mutexes): make test-race — CI runs the race detector and make devcheck does not, so this is the most common green-local/red-CI surprise
  • after any dependency change (adding/removing an import, editing go.mod): make tidy-check — CI fails on an untidy go.mod/go.sum even when devcheck passes
  • before opening a PR you want green on the first push: make ci (devcheck + test-race + tidy-check, the full local CI mirror; slow)
  • if touching internal/ui/, internal/vterm/, or cmd/amux-harness/: make harness-presets
  • if touching internal/tmux/, internal/e2e/, or internal/pty/: go test ./internal/tmux ./internal/e2e
  • if touching the agent input/send path (internal/pty/terminal.go, internal/ui/center/tab_actor_write.go, internal/pty/, agent keystroke forwarding): make verify-loop — proves a real agent receives keystrokes end-to-end (incl. a literal CR); make devcheck does not, since the real-tmux tests skip there

Architecture references:

  • ARCHITECTURE.md (repo-level package map and dependency direction)
  • internal/app/ARCHITECTURE.md
  • internal/app/MESSAGE_FLOW.md

Harness

cmd/amux-harness renders the real UI without a TTY for deterministic perf and render checks. make harness-presets runs heavier local confidence presets for center/sidebar/monitor. CI uses shorter direct invocations; to reproduce a CI failure, run the matching mode with the CI shape, e.g. center:

go run ./cmd/amux-harness -mode center -frames 5 -warmup 1 -tabs 8 -width 160 -height 48 -hot-tabs 2 -payload-bytes 64 -newline-every 4

Inspecting a rendered frame

To see exactly what the UI rendered (instead of guessing), dump the final frame with -dump-frame:

go run ./cmd/amux-harness -mode center -frames 1 -warmup 0 -dump-frame /tmp/frame.txt

The file contains the raw ANSI bytes the agent sees — cat /tmp/frame.txt to eyeball it, diff two dumps to spot a regression, or feed it into a golden.

Rendering an overlay

Adding or altering a dialog/overlay is the most common UI change. The harness can put the App into an overlay state so the frame exercises composeOverlays instead of only the base pane. Pass -overlay (or set HarnessOptions.Overlay):

go run ./cmd/amux-harness -mode center -frames 1 -warmup 0 -overlay dialog -dump-frame /tmp/frame.txt

Supported overlays are the deterministic, filesystem-independent ones: dialog (confirm dialog), settings (settings dialog), prefix (prefix command palette), error (the error overlay), and input (input dialog). The file picker (reads the real filesystem) and the toast (wall-clock-gated visibility) are intentionally excluded because their frames are not byte-stable. Each overlay has a golden frame (internal/app/testdata/golden/overlay_*.frame) guarded by TestHarnessGoldenFrames; regenerate after an intentional overlay render change with go test ./internal/app -run Golden -update and commit the refreshed testdata.

See go doc ./cmd/amux-harness for all -mode values, flags, and the AMUX_PPROF profiling hook.

Release

Versioning follows SemVer and tags are vX.Y.Z. Pushing a tag triggers the GitHub Actions release job.

Fast path:

git pull --ff-only
make release VERSION=v0.0.5

Manual steps:

make release-check
git tag -a v0.0.5 -m "v0.0.5"
git push origin v0.0.5

Notes:

  • make release runs release-check, creates an annotated tag, and pushes it. The worktree must be clean.
  • Release builds use the commit timestamp for main.date, which keeps the timestamp deterministic for a given commit. If you need strict bit-for-bit reproducibility, consider adding -trimpath and a stable build ID to the build flags.

Homebrew tap

The Homebrew tap lives in andyrewlee/homebrew-amux and auto-bumps the formula after a release.

  • After make release VERSION=vX.Y.Z, the tap workflow updates Formula/amux.rb (daily at 06:00 UTC).
  • To update immediately, run the Bump amux formula workflow in the tap repo.
  • Users upgrade with brew upgrade amux.