git-stack is a CLI (git stack <cmd>) and stdio MCP server for GitHub's native stacked PRs, written in Go. It sits on top of gh stack.
docs/architecture.md holds the design, what we learnt about gh stack and the decisions behind it; read it before large changes.
- Go 1.27. Never lower the
godirective. Use modern stdlib (iterators, slices, maps,encoding/json/v2,errors.AsType) over hand-rolled helpers. - Before finishing any change, all of these must pass (there is no task runner on purpose):
gofmt -l .(empty),go vet ./...,golangci-lint run,go test ./....
cmd/: cobra wiring only, one command per file (cmd/<command>.go). No business logic: parse flags, decide TTY vs non-TTY, callpkg/app, render output.pkg/app: use cases. Bothcmd/andpkg/mcpcall these; never duplicate logic between CLI and MCP.- Ports live with their domain (
pkg/stack,pkg/forge,pkg/ai). Adapters live in subpackages (pkg/backend/ghstack,pkg/forge/github,pkg/ai/claudecode). - Only
cmd/root.go(andcmd/mcp.gofor server wiring) may import adapters. Nothing inpkg/importscmd/. Adapters never import each other. - No vendor-specific types (GitHub, Anthropic, gh-stack) in port signatures.
- All TUI code lives in
pkg/uiand uses Charm v2 (charm.land/bubbletea/v2,charm.land/lipgloss/v2,charm.land/bubbles/v2). No v1 imports. cmd/imports_test.goenforces the layering rules above; don't weaken it.
- All subprocesses go through
pkg/exec. Never callos/execdirectly elsewhere. - Default to capturing output. TTY passthrough is opt-in, CLI-only, and forbidden in any code path reachable from
pkg/mcp. - Always pass a
context.Contextand honour cancellation. - A non-zero exit is returned as
*exec.ExitError(with captured stderr); a missing binary as*exec.NotFoundError. Adapters map these to*stack.Errorwith aKindandNextSteps.
- Every command has
ValidArgsFunction; every non-bool flag has a completion func. The completion-coverage test enforces this; don't weaken it. - Completion code must not touch the network, prompt, or write non-completion output. It may only run
git. - Keep command names, flags and short aliases familiar to people coming from other stacking tools where practical, but describe behaviour in our own words in help text and docs; never present a command as a copy of another tool's. Don't add flags the backend can't support. When behaviour differs because of a backend limitation, say so in one line of output rather than silently diverging.
- Every command works non-interactively (
--no-interactiveor no TTY). - Every human facing line goes through
ui.Reporter(results on stdout, notices, progress and errors on stderr) and followsdocs/style.md;cmd/style_test.gofails the build on rawfmt.Fprintincmd/.
- Stdout is reserved for JSON-RPC. Log to stderr. Capture all subprocess stdout.
- MCP tools never prompt, never launch a TUI, and return structured, actionable errors.
- New CLI capabilities should get a matching MCP tool (or a documented reason why not).
- Read and write git config via
git config, never by editing files as text. - Never edit
~/.claude.jsonor~/.codex/config.tomldirectly; useclaude mcp/codex mcp. installmust never clobber existing user aliases or config without confirmation, and--uninstallremoves only what we recorded as managed.- Only
pkg/backend/ghstackwrites gh-stack's<git-dir>/gh-stackfile, through itsUpdateundergh-stack.lock, editing the JSON in place; nothing else writes it, andgh stackcommands do every other mutation.
- Tests must never touch the real user environment. Use
gittest.Isolate(t)(setsHOME,XDG_CONFIG_HOME,GIT_CONFIG_GLOBAL,GIT_CONFIG_NOSYSTEM) in any test that runs git or installers. gh,claudeandcodexare faked withexectest.Fakein unit tests and with recording executables onPATHin integration tests. No network in the default test run; live tests sit behind thelivebuild tag.- Domain logic gets table-driven unit tests;
pkg/gitgets integration tests against temp repos; TUIs get teatest golden snapshots with fixed size and colour profile; MCP tools get in-memory-transport tests. - Shell completion hooks are verified against real shells with
go test -tags shellintegration ./pkg/shell/(fish, bash; zsh via zpty). Run it after touchingpkg/shell/hooks.
- Small, focused commits; each leaves the tree green.
- Update
README.md(command table) anddocs/in the same change as behaviour changes. - Pushing a stack and opening or updating its PRs (
git stack submit/stack_submit) is fine. Never merge a PR or a stack (git stack merge/stack_merge,gh pr merge) unless Dom has said so for that stack, and don't create remotes.